From 90d6e98b51b67b299ad3e46d51748e3b59ad4452 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:23:52 +0800 Subject: [PATCH 001/384] fix(flutter_mobile): drop focus on tap outside (#7274) --- frontend/appflowy_flutter/ios/Podfile.lock | 44 +++++++++---------- .../chat_input/mobile_chat_input.dart | 23 +++++----- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index e4e87805cd..46b13d240d 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -175,36 +175,36 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart index 9b667889e9..06694e3aa3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart @@ -138,16 +138,18 @@ class _MobileChatInputState extends State { else const VSpace(8.0), inputTextField(context), - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const HSpace(8.0), - leadingButtons(context), - const Spacer(), - sendButton(), - const HSpace(12.0), - ], + TextFieldTapRegion( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const HSpace(8.0), + leadingButtons(context), + const Spacer(), + sendButton(), + const HSpace(12.0), + ], + ), ), ), ], @@ -281,6 +283,7 @@ class _MobileChatInputState extends State { fontWeight: FontWeight.w600, ), ), + onTapOutside: (_) => focusNode.unfocus(), ); }, ); From 9271d42db576890c3fbca44e61410e77c3d0b2b3 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sat, 1 Feb 2025 23:48:32 +0800 Subject: [PATCH 002/384] chore: fetch model list (#7306) * chore: fetch model list --- .../settings/ai/settings_ai_bloc.dart | 49 +++++++++++++++++-- .../setting_ai_view/model_selection.dart | 38 ++------------ frontend/rust-lib/Cargo.lock | 42 ++++++++++------ frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 4 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 11 ++++- frontend/rust-lib/flowy-ai/src/entities.rs | 6 +++ .../rust-lib/flowy-ai/src/event_handler.rs | 9 ++++ frontend/rust-lib/flowy-ai/src/event_map.rs | 7 +++ .../src/middleware/chat_service_mw.rs | 10 ++-- .../src/deps_resolve/cloud_service_impl.rs | 10 +++- .../flowy-server/src/af_cloud/impls/chat.rs | 11 ++++- .../flowy-server/src/af_cloud/server.rs | 4 +- .../rust-lib/flowy-server/src/default_impl.rs | 6 ++- .../flowy-user/src/entities/user_profile.rs | 14 +++--- .../flowy-user/src/entities/workspace.rs | 45 ++--------------- .../user_manager/manager_user_workspace.rs | 4 +- 17 files changed, 162 insertions(+), 112 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 91ac63944c..6d0f396424 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -9,6 +11,7 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; +part 'settings_ai_bloc.g.dart'; class SettingsAIBloc extends Bloc { SettingsAIBloc( @@ -65,6 +68,7 @@ class SettingsAIBloc extends Bloc { }, ); _loadUserWorkspaceSetting(); + _loadModelList(); }, didReceiveUserProfile: (userProfile) { emit(state.copyWith(userProfile: userProfile)); @@ -78,7 +82,7 @@ class SettingsAIBloc extends Bloc { !(state.aiSettings?.disableSearchIndexing ?? false), ); }, - selectModel: (AIModelPB model) { + selectModel: (String model) { _updateUserWorkspaceSetting(model: model); }, didLoadAISetting: (UseAISettingPB settings) { @@ -89,6 +93,14 @@ class SettingsAIBloc extends Bloc { ), ); }, + didLoadAvailableModels: (String models) { + final dynamic decodedJson = jsonDecode(models); + Log.info("Available models: $decodedJson"); + if (decodedJson is Map) { + final models = ModelList.fromJson(decodedJson).models; + emit(state.copyWith(availableModels: models)); + } + }, refreshMember: (member) { emit(state.copyWith(currentWorkspaceMemberRole: member.role)); }, @@ -98,7 +110,7 @@ class SettingsAIBloc extends Bloc { void _updateUserWorkspaceSetting({ bool? disableSearchIndexing, - AIModelPB? model, + String? model, }) { final payload = UpdateUserWorkspaceSettingPB( workspaceId: workspaceId, @@ -132,6 +144,18 @@ class SettingsAIBloc extends Bloc { }); }); } + + void _loadModelList() { + AIEventGetAvailableModels().send().then((result) { + result.fold((config) { + if (!isClosed) { + add(SettingsAIEvent.didLoadAvailableModels(config.models)); + } + }, (err) { + Log.error(err); + }); + }); + } } @freezed @@ -145,11 +169,15 @@ class SettingsAIEvent with _$SettingsAIEvent { const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = _RefreshMember; - const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; + const factory SettingsAIEvent.selectModel(String model) = _SelectAIModel; const factory SettingsAIEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; + + const factory SettingsAIEvent.didLoadAvailableModels( + String models, + ) = _DidLoadAvailableModels; } @freezed @@ -158,6 +186,21 @@ class SettingsAIState with _$SettingsAIState { required UserProfilePB userProfile, UseAISettingPB? aiSettings, AFRolePB? currentWorkspaceMemberRole, + @Default(["default"]) List availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } + +@JsonSerializable() +class ModelList { + ModelList({ + required this.models, + }); + + factory ModelList.fromJson(Map json) => + _$ModelListFromJson(json); + + final List models; + + Map toJson() => _$ModelListToJson(this); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 22aaf0bcca..e03aa639e4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -4,8 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -30,18 +28,18 @@ class AIModelSelection extends StatelessWidget { ), const Spacer(), Flexible( - child: SettingsDropdown( + child: SettingsDropdown( key: const Key('_AIModelSelection'), onChanged: (model) => context .read() .add(SettingsAIEvent.selectModel(model)), selectedOption: state.userProfile.aiModel, - options: _availableModels + options: state.availableModels .map( - (format) => buildDropdownMenuEntry( + (model) => buildDropdownMenuEntry( context, - value: format, - label: _titleForAIModel(format), + value: model, + label: model, ), ) .toList(), @@ -54,29 +52,3 @@ class AIModelSelection extends StatelessWidget { ); } } - -List _availableModels = [ - AIModelPB.DefaultModel, - AIModelPB.Claude3Opus, - AIModelPB.Claude3Sonnet, - AIModelPB.GPT4oMini, - AIModelPB.GPT4o, -]; - -String _titleForAIModel(AIModelPB model) { - switch (model) { - case AIModelPB.DefaultModel: - return "Default"; - case AIModelPB.Claude3Opus: - return "Claude 3 Opus"; - case AIModelPB.Claude3Sonnet: - return "Claude 3 Sonnet"; - case AIModelPB.GPT4oMini: - return "GPT-4o-mini"; - case AIModelPB.GPT4o: - return "GPT-4o"; - default: - Log.error("Unknown AI model: $model, fallback to default"); - return "Default"; - } -} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index c26bb535ff..733a784e42 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "bytes", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "futures-channel", "futures-util", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "bincode", @@ -1153,7 +1153,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "async-trait", @@ -1400,7 +1400,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1548,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "app-error", @@ -2970,7 +2970,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "futures-util", @@ -2987,7 +2987,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "app-error", @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "bytes", @@ -4624,7 +4624,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4644,6 +4644,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4711,6 +4712,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6140,7 +6154,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8435b0f904..b93d19954b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4a26572a4e43714def9b362d444c640fdf1bc0d9" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4a26572a4e43714def9b362d444c640fdf1bc0d9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "82409199f8ffa0166f2f5d9403ccd55831890549" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "82409199f8ffa0166f2f5d9403ccd55831890549" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index bf787f3d20..98198c8f9f 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,7 +1,7 @@ use bytes::Bytes; pub use client_api::entity::ai_dto::{ AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionType, CreateChatContext, - LLMModel, LocalAIConfig, ModelInfo, OutputContent, OutputLayout, RelatedQuestion, + LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, }; pub use client_api::entity::billing_dto::SubscriptionPlan; @@ -119,4 +119,6 @@ pub trait ChatCloudService: Send + Sync + 'static { chat_id: &str, params: UpdateChatParams, ) -> Result<(), FlowyError>; + + async fn get_available_models(&self, workspace_id: &str) -> Result; } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index c88a26504c..5a784f6db6 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, UpdateChatParams}; +use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, ModelList, UpdateChatParams}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; @@ -241,6 +241,15 @@ impl AIManager { Ok(()) } + pub async fn get_available_models(&self) -> FlowyResult { + let workspace_id = self.user_service.workspace_id()?; + let list = self + .cloud_service_wm + .get_available_models(&workspace_id) + .await?; + Ok(list) + } + pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result, FlowyError> { let chat = self.chats.get(chat_id).as_deref().cloned(); match chat { diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index a5b9778f06..6a30a7dff7 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -182,6 +182,12 @@ pub struct ChatMessageListPB { pub total: i64, } +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct ModelConfigPB { + #[pb(index = 1)] + pub models: String, +} + impl From for ChatMessageListPB { fn from(repeated_chat_message: RepeatedChatMessage) -> Self { let messages = repeated_chat_message diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index d8ebd0e93b..baa0898220 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -107,6 +107,15 @@ pub(crate) async fn regenerate_response_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_available_model_list_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let models = serde_json::to_string(&ai_manager.get_available_models().await?)?; + data_result_ok(ModelConfigPB { models }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn load_prev_message_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index c27e94c334..2bd2bee863 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -60,6 +60,10 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::GetChatSettings, get_chat_settings_handler) .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) .event(AIEvent::RegenerateResponse, regenerate_response_handler) + .event( + AIEvent::GetAvailableModels, + get_available_model_list_handler, + ) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -154,4 +158,7 @@ pub enum AIEvent { #[event(input = "RegenerateResponsePB")] RegenerateResponse = 27, + + #[event(output = "ModelConfigPB")] + GetAvailableModels = 28, } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index ae8ce9b8d0..d6dce3696c 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -10,9 +10,9 @@ use std::collections::HashMap; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, LocalAIConfig, MessageCursor, RelatedQuestion, RepeatedChatMessage, - RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, - UpdateChatParams, + CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RelatedQuestion, + RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, + SubscriptionPlan, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; @@ -353,4 +353,8 @@ impl ChatCloudService for AICloudServiceMiddleware { .update_chat_settings(workspace_id, chat_id, params) .await } + + async fn get_available_models(&self, workspace_id: &str) -> Result { + self.cloud_service.get_available_models(workspace_id).await + } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index b01ea49f82..22342615e3 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -22,7 +22,7 @@ use collab_integrate::collab_builder::{ }; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, LocalAIConfig, MessageCursor, RepeatedChatMessage, ResponseFormat, + CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_database_pub::cloud::{ @@ -838,6 +838,14 @@ impl ChatCloudService for ServerProvider { .update_chat_settings(workspace_id, chat_id, params) .await } + + async fn get_available_models(&self, workspace_id: &str) -> Result { + self + .get_server()? + .chat_service() + .get_available_models(workspace_id) + .await + } } #[async_trait] diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 7512b9e48c..11fc5cc27c 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -8,7 +8,7 @@ use client_api::entity::chat_dto::{ }; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, LocalAIConfig, - StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; @@ -270,4 +270,13 @@ where .await?; Ok(()) } + + async fn get_available_models(&self, workspace_id: &str) -> Result { + let list = self + .inner + .try_get_client()? + .get_model_list(workspace_id) + .await?; + Ok(list) + } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index ee908ac9cc..06e56a8c05 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,4 +1,3 @@ -use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -7,7 +6,6 @@ use crate::af_cloud::define::ServerUser; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; -use client_api::entity::ai_dto::AIModel; use client_api::entity::UserMessage; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ @@ -124,7 +122,7 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn set_ai_model(&self, ai_model: &str) -> Result<(), Error> { - self.client.set_ai_model(AIModel::from_str(ai_model)?); + self.client.set_ai_model(ai_model.to_string()); Ok(()) } diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 792db6e23a..194aa89ef3 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,7 +1,7 @@ use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, MessageCursor, RepeatedChatMessage, ResponseFormat, StreamAnswer, + CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; @@ -144,4 +144,8 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } + + async fn get_available_models(&self, _workspace_id: &str) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index facc9d8b41..aa9d38a9cd 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,13 +1,11 @@ -use std::convert::TryInto; -use std::str::FromStr; - use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; use lib_infra::validator_fn::required_not_empty_str; +use std::convert::TryInto; use validator::Validate; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; -use crate::entities::{AIModelPB, AuthenticatorPB}; +use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; use super::parser::UserStabilityAIKey; @@ -58,7 +56,7 @@ pub struct UserProfilePB { pub stability_ai_key: String, #[pb(index = 11)] - pub ai_model: AIModelPB, + pub ai_model: String, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -79,6 +77,10 @@ impl From for UserProfilePB { EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), }; + let mut ai_model = user_profile.ai_model; + if ai_model.is_empty() { + ai_model = "Default".to_string(); + } Self { id: user_profile.uid, email: user_profile.email, @@ -90,7 +92,7 @@ impl From for UserProfilePB { encryption_sign, encryption_type: encryption_ty, stability_ai_key: user_profile.stability_ai_key, - ai_model: AIModelPB::from_str(&user_profile.ai_model).unwrap_or_default(), + ai_model, } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 736979f11a..240b153e33 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -3,7 +3,6 @@ use client_api::entity::billing_dto::{ WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit, }; use serde::{Deserialize, Serialize}; -use std::str::FromStr; use validator::Validate; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; @@ -382,14 +381,14 @@ pub struct UseAISettingPB { pub disable_search_indexing: bool, #[pb(index = 2)] - pub ai_model: AIModelPB, + pub ai_model: String, } impl From for UseAISettingPB { fn from(value: AFWorkspaceSettings) -> Self { Self { disable_search_indexing: value.disable_search_indexing, - ai_model: AIModelPB::from_str(&value.ai_model).unwrap_or_default(), + ai_model: value.ai_model, } } } @@ -404,7 +403,7 @@ pub struct UpdateUserWorkspaceSettingPB { pub disable_search_indexing: Option, #[pb(index = 3, one_of)] - pub ai_model: Option, + pub ai_model: Option, } impl From for AFWorkspaceSettingsChange { @@ -414,48 +413,12 @@ impl From for AFWorkspaceSettingsChange { change = change.disable_search_indexing(disable_search_indexing); } if let Some(ai_model) = value.ai_model { - change = change.ai_model(ai_model.to_str().to_string()); + change = change.ai_model(ai_model); } change } } -#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)] -pub enum AIModelPB { - #[default] - DefaultModel = 0, - GPT4oMini = 1, - GPT4o = 2, - Claude3Sonnet = 3, - Claude3Opus = 4, -} - -impl AIModelPB { - pub fn to_str(&self) -> &str { - match self { - AIModelPB::DefaultModel => "default-model", - AIModelPB::GPT4oMini => "gpt-4o-mini", - AIModelPB::GPT4o => "gpt-4o", - AIModelPB::Claude3Sonnet => "claude-3-sonnet", - AIModelPB::Claude3Opus => "claude-3-opus", - } - } -} - -impl FromStr for AIModelPB { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "gpt-3.5-turbo" => Ok(AIModelPB::GPT4oMini), - "gpt-4o" => Ok(AIModelPB::GPT4o), - "claude-3-sonnet" => Ok(AIModelPB::Claude3Sonnet), - "claude-3-opus" => Ok(AIModelPB::Claude3Opus), - _ => Ok(AIModelPB::DefaultModel), - } - } -} - #[derive(Debug, ProtoBuf, Default, Clone)] pub struct WorkspaceSubscriptionInfoPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 31ad71ce74..e666d2486a 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -559,12 +559,12 @@ impl UserManager { .send(); if let Some(ai_model) = &ai_model { - if let Err(err) = self.cloud_services.set_ai_model(ai_model.to_str()) { + if let Err(err) = self.cloud_services.set_ai_model(ai_model) { error!("Set ai model failed: {}", err); } let conn = self.db_connection(uid)?; - let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model.to_str()); + let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model); upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; } Ok(()) From 36349778e32728d7559e9127e253537fbad100d1 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Momeni Date: Mon, 3 Feb 2025 05:35:34 +0330 Subject: [PATCH 003/384] chore(i18n): update fa translations (#7292) --- frontend/resources/translations/ar-SA.json | 4 +-- frontend/resources/translations/en.json | 1 - frontend/resources/translations/fa.json | 30 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 5e1cbf853c..d779063b6b 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -2190,11 +2190,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل" + "clickToOpen": "انقر لفتح السجل", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 09d2cf4e05..ab5e606eb2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -901,7 +901,6 @@ "description": "Unlimited AI responses powered by advanced AI models, and 50 AI images per month", "price": "{}", "priceInfo": "Per user per month billed annually" - }, "aiOnDevice": { "title": "AI On-device for Mac", diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 21c8505c88..80a100c3bc 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -2,12 +2,14 @@ "appName": "AppFlowy", "defaultUsername": "من", "welcomeText": "به @:appName خوش آمدید", + "welcomeTo": "خوش آمدید به", "githubStarText": "به گیت‌هاب ما ستاره دهید", "subscribeNewsletterText": "اشتراک در خبرنامه", "letsGoButtonText": "شروع کنید", "title": "عنوان", "youCanAlso": "همچنین می‌توانید", "and": "و", + "failedToOpenUrl": "خطا در بازکردن نشانی وب: {}", "blockActions": { "addBelowTooltip": "برای افزودن در زیر کلیک کنید", "addAboveCmd": "Alt+click", @@ -32,19 +34,47 @@ "signIn": { "loginTitle": "ورود به @:appName", "loginButtonText": "ورود", + "loginStartWithAnonymous": "ادامه دادن با یک جلسه ناشناس", "continueAnonymousUser": "ادامه دادن به صورت کاربر مهمان", + "anonymous": "ناشناس", "buttonText": "ورود", + "signingInText": "در حال ورود...", "forgotPassword": "رمز عبور را فراموش کرده اید؟", "emailHint": "ایمیل", "passwordHint": "رمز عبور", "dontHaveAnAccount": "آیا حساب کاربری ندارید؟", + "createAccount": "ساخت حساب کاربری", "repeatPasswordEmptyError": "تکرار رمز عبور نمی‌تواند خالی باشد", "unmatchedPasswordError": "تکرار رمز عبور مشابه رمز عبور نیست", + "syncPromptMessage": "همگام سازی داده ها ممکن است کمی طول بکشد. لطفا این صفحه را نبندید", + "or": "یا", + "signInWithGoogle": "ادامه دادن با گوگل", + "signInWithGithub": "ادامه دادن با گیتهاب", + "signInWithDiscord": "ادامه دادن با دیسکورد", + "signInWithApple": "ادامه دادن با اپل", + "continueAnotherWay": "ادامه دادن از طریق دیگر", + "signUpWithGoogle": "ثبت نام با گوگل", + "signUpWithGithub": "ثبت نام با گیتهاب", + "signUpWithDiscord": "ثبت نام با دیسکورد", "signInWith": "ثبت نام با:", + "signInWithEmail": "ادامه دادن با ایمیل", + "signInWithMagicLink": "ادامه", + "pleaseInputYourEmail": "لطفا آدرس ایمیل خود را وارد کنید", + "settings": "تنظیمات", + "invalidEmail": "لطفا یک آدرس ایمیل معتبر وارد کنید", + "alreadyHaveAnAccount": "حساب کاربری دارید؟", + "logIn": "ورود", + "generalError": "مشکلی پیش آمد. لطفاً بعداً دوباره امتحان کنید", "loginAsGuestButtonText": "شروع کنید" }, "workspace": { + "chooseWorkspace": "فضای کار خود را انتخاب کنید", + "defaultName": "فضای کار من", "create": "ایجاد فضای کار", + "new": "فضای کار جدید", + "learnMore": "بیشتر بدانید", + "renameWorkspace": "حذف فضای کار", + "workspaceNameCannotBeEmpty": "اسم فضای کار نمی‌تواند خالی باشد", "hint": "فضای کار", "notFoundError": "فضای کاری پیدا نشد" }, From 25a27dfa817680653430168912df53251b3137ce Mon Sep 17 00:00:00 2001 From: Peter Jose <34370936+ptrjs@users.noreply.github.com> Date: Mon, 3 Feb 2025 09:41:05 +0700 Subject: [PATCH 004/384] chore(i18n): update id-ID translations (#7290) - Translate 'themeMode' label - Adjust text 'fontFamily' - Update 'layoutDirection' translation - Update 'textDirection' translation --- frontend/resources/translations/id-ID.json | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index ede3571878..ec87ffabd0 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -206,13 +206,13 @@ "addBlockBelow": "Tambahkan blok di bawah ini" }, "sideBar": { - "closeSidebar": "Close sidebar", - "openSidebar": "Open sidebar", + "closeSidebar": "Tutup sidebar", + "openSidebar": "Buka sidebar", "personal": "Pribadi", "favorites": "Favorit", - "clickToHidePersonal": "Klik untuk menutup seksi pribadi", - "clickToHideFavorites": "Klik untuk menutup seksi favorit", - "addAPage": "Tambah sebuah page" + "clickToHidePersonal": "Klik untuk menutup Pribadi", + "clickToHideFavorites": "Klik untuk menutup Favorit", + "addAPage": "Tambah halaman baru" }, "notifications": { "export": { @@ -305,28 +305,28 @@ "appearance": { "resetSetting": "Mengatur ulang pengaturan ini", "fontFamily": { - "label": "Keluarga Fon", - "search": "Mencari" + "label": "Jenis Font", + "search": "Cari" }, "themeMode": { - "label": "Theme Mode", - "light": "Mode Terang", - "dark": "Mode Gelap", - "system": "Adapt to System" + "label": "Tema", + "light": "Terang", + "dark": "Gelap", + "system": "Sesuai Sistem" }, "layoutDirection": { - "label": "Arah Layout", - "hint": "Mengontrol aliran konten pada layar Anda, dari kiri ke kanan atau kanan ke kiri.", - "ltr": "LTR", - "rtl": "RTL" + "label": "Arah Tampilan", + "hint": "Mengatur arah tampilan konten, apakah dari kiri ke kanan atau kanan ke kiri.", + "ltr": "Kiri ke Kanan", + "rtl": "Kanan ke Kiri" }, - "textDirection": { - "label": "Arah teks default", - "hint": "Menentukan apakah teks harus dimulai dari kiri atau kanan sebagai default.", - "ltr": "LTR", - "rtl": "RTL", - "auto": "AUTO", - "fallback": "Sama seperti arah layout" + "textDirection": { + "label": "Arah Teks Bawaan", + "hint": "Atur arah teks bawaan, apakah dari kiri ke kanan atau kanan ke kiri.", + "ltr": "Kiri ke Kanan", + "rtl": "Kanan ke Kiri", + "auto": "Otomatis", + "fallback": "Sesuai Arah Tampilan" }, "themeUpload": { "button": "Mengunggah", From aacd09d8e20bd18fcba91095357cc987bf65bb7d Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:52:08 +0800 Subject: [PATCH 005/384] chore: Support new error code (#7311) * chore: fetch model list * chore: suppor new error code --- .../lib/ai/service/appflowy_ai_service.dart | 9 ++++ .../appflowy_flutter/lib/env/cloud_env.dart | 1 + .../application/chat_ai_message_bloc.dart | 16 +++++++ .../application/chat_message_stream.dart | 22 ++++++++++ .../presentation/message/ai_text_message.dart | 5 +++ .../settings/ai/settings_ai_bloc.dart | 24 ++++++++++- .../setting_ai_view/model_selection.dart | 2 +- frontend/rust-lib/Cargo.lock | 42 +++++++------------ frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 4 ++ .../rust-lib/flowy-ai/src/event_handler.rs | 10 ++++- frontend/rust-lib/flowy-error/src/code.rs | 3 ++ frontend/rust-lib/flowy-error/src/errors.rs | 6 ++- .../flowy-error/src/impl_from/cloud.rs | 1 + 14 files changed, 114 insertions(+), 35 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 560d6d3ef1..540e9f2804 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -148,6 +148,15 @@ class CompletionStream { ); } + if (event.startsWith("AI_MAX_REQUIRED:")) { + final msg = event.substring(16); + onError( + AIError( + message: msg, + ), + ); + } + if (event.startsWith("start:")) { await onStart(); } diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 8f4d195eb7..3df88eac24 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -247,6 +247,7 @@ Future configurationFromUri( // In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports. // This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server // in the development environment. + // If you modify following code, please update the corresponding documentation in the appflowy billing. if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) { return AppFlowyCloudConfiguration( base_url: "$baseUrl:8000", diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index f3bef96418..60fca000c0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -105,6 +105,13 @@ class ChatAIMessageBloc extends Bloc { ), ); }, + onAIMaxRequired: (message) { + emit( + state.copyWith( + messageState: MessageState.onAIMaxRequired(message), + ), + ); + }, receiveMetadata: (metadata) { Log.debug("AI Steps: ${metadata.progress?.step}"); emit( @@ -146,6 +153,12 @@ class ChatAIMessageBloc extends Bloc { add(ChatAIMessageEvent.receiveMetadata(metadata)); } }, + onAIMaxRequired: (message) { + if (!isClosed) { + Log.info(message); + add(ChatAIMessageEvent.onAIMaxRequired(message)); + } + }, ); } } @@ -159,6 +172,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent { const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit; const factory ChatAIMessageEvent.onAIImageResponseLimit() = _OnAIImageResponseLimit; + const factory ChatAIMessageEvent.onAIMaxRequired(String message) = + _OnAIMaxRquired; const factory ChatAIMessageEvent.receiveMetadata( MetadataCollection metadata, ) = _ReceiveMetadata; @@ -193,6 +208,7 @@ class MessageState with _$MessageState { const factory MessageState.onError(String error) = _Error; const factory MessageState.onAIResponseLimit() = _AIResponseLimit; const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit; + const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired; const factory MessageState.ready() = _Ready; const factory MessageState.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart index c675780838..df6c1993a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart @@ -28,6 +28,14 @@ class AnswerStream { } else if (event == "AI_IMAGE_RESPONSE_LIMIT") { _aiImageLimitReached = true; _onAIImageResponseLimit?.call(); + } else if (event.startsWith("AI_MAX_REQUIRED:")) { + final msg = event.substring(16); + // If the callback is not registered yet, add the event to the buffer. + if (_onAIMaxRequired != null) { + _onAIMaxRequired!(msg); + } else { + _pendingAIMaxRequiredEvents.add(msg); + } } }, onDone: () { @@ -56,8 +64,12 @@ class AnswerStream { void Function(String error)? _onError; void Function()? _onAIResponseLimit; void Function()? _onAIImageResponseLimit; + void Function(String message)? _onAIMaxRequired; void Function(MetadataCollection metadataCollection)? _onMetadata; + // Buffer for events that occur before listen() is called. + final List _pendingAIMaxRequiredEvents = []; + int get nativePort => _port.sendPort.nativePort; bool get hasStarted => _hasStarted; bool get aiLimitReached => _aiLimitReached; @@ -78,6 +90,7 @@ class AnswerStream { void Function(String error)? onError, void Function()? onAIResponseLimit, void Function()? onAIImageResponseLimit, + void Function(String message)? onAIMaxRequired, void Function(MetadataCollection metadata)? onMetadata, }) { _onData = onData; @@ -87,6 +100,15 @@ class AnswerStream { _onAIResponseLimit = onAIResponseLimit; _onAIImageResponseLimit = onAIImageResponseLimit; _onMetadata = onMetadata; + _onAIMaxRequired = onAIMaxRequired; + + // Flush any buffered AI_MAX_REQUIRED events. + if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) { + for (final msg in _pendingAIMaxRequiredEvents) { + _onAIMaxRequired!(msg); + } + _pendingAIMaxRequiredEvents.clear(); + } _onStart?.call(); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index f36a77c0ee..b0aa995107 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -137,6 +137,11 @@ class ChatAIMessageWidget extends StatelessWidget { errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(), ); }, + onAIMaxRequired: (message) { + return ChatErrorMessageWidget( + errorMessage: message, + ); + }, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 6d0f396424..8a4696606b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -22,6 +22,7 @@ class SettingsAIBloc extends Bloc { _userService = UserBackendService(userId: userProfile.id), super( SettingsAIState( + selectedAIModel: userProfile.aiModel, userProfile: userProfile, currentWorkspaceMemberRole: currentWorkspaceMemberRole, ), @@ -98,7 +99,25 @@ class SettingsAIBloc extends Bloc { Log.info("Available models: $decodedJson"); if (decodedJson is Map) { final models = ModelList.fromJson(decodedJson).models; - emit(state.copyWith(availableModels: models)); + if (models.isEmpty) { + // If available models is empty, then we just show the + // Default + emit(state.copyWith(availableModels: ["Default"])); + return; + } + + if (!models.contains(state.selectedAIModel)) { + // Use first model as default model if current selected model + // is not available + emit( + state.copyWith( + availableModels: models, + selectedAIModel: models[0], + ), + ); + } else { + emit(state.copyWith(availableModels: models)); + } } }, refreshMember: (member) { @@ -185,8 +204,9 @@ class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, UseAISettingPB? aiSettings, + @Default("Default") String selectedAIModel, AFRolePB? currentWorkspaceMemberRole, - @Default(["default"]) List availableModels, + @Default(["Default"]) List availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index e03aa639e4..dfc53e4f08 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -33,7 +33,7 @@ class AIModelSelection extends StatelessWidget { onChanged: (model) => context .read() .add(SettingsAIEvent.selectModel(model)), - selectedOption: state.userProfile.aiModel, + selectedOption: state.selectedAIModel, options: state.availableModels .map( (model) => buildDropdownMenuEntry( diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 733a784e42..88d0e9d6f8 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "bytes", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "futures-channel", "futures-util", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "bincode", @@ -1153,7 +1153,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "async-trait", @@ -1400,7 +1400,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1548,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "app-error", @@ -2970,7 +2970,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "futures-util", @@ -2987,7 +2987,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "app-error", @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "bytes", @@ -4624,7 +4624,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4644,7 +4644,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4712,19 +4711,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6154,7 +6140,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=82409199f8ffa0166f2f5d9403ccd55831890549#82409199f8ffa0166f2f5d9403ccd55831890549" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b93d19954b..312088862e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "82409199f8ffa0166f2f5d9403ccd55831890549" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "82409199f8ffa0166f2f5d9403ccd55831890549" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index f6b90e9b00..3c18ee0b9f 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -259,6 +259,10 @@ impl Chat { let _ = answer_sink .send("AI_IMAGE_RESPONSE_LIMIT".to_string()) .await; + } else if err.is_ai_max_required() { + let _ = answer_sink + .send(format!("AI_MAX_REQUIRED:{}", err.msg)) + .await; } else { let _ = answer_sink.send(format!("error:{}", err)).await; } diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index baa0898220..746a843da5 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -13,6 +13,7 @@ use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, Con use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use lib_infra::isolate_stream::IsolateSink; +use serde_json::json; use std::sync::{Arc, Weak}; use tracing::trace; use validator::Validate; @@ -112,7 +113,14 @@ pub(crate) async fn get_available_model_list_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let models = serde_json::to_string(&ai_manager.get_available_models().await?)?; + let available_models = ai_manager.get_available_models().await?; + let models = available_models + .models + .into_iter() + .map(|m| m.name) + .collect::>(); + + let models = serde_json::to_string(&json!({"models": models}))?; data_result_ok(ModelConfigPB { models }) } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 36c7b06e6d..8ed2f3b4ea 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -362,6 +362,9 @@ pub enum ErrorCode { #[error("AI Image Response limit exceeded")] AIImageResponseLimitExceeded = 124, + + #[error("AI Max Required")] + AIMaxRequired = 125, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index d7b085a803..34965e036e 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -13,7 +13,7 @@ use crate::code::ErrorCode; pub type FlowyResult = anyhow::Result; #[derive(Debug, Default, Clone, ProtoBuf, Error)] -#[error("{msg}")] +#[error("code:{code}, message:{msg}")] pub struct FlowyError { #[pb(index = 1)] pub code: ErrorCode, @@ -95,6 +95,10 @@ impl FlowyError { self.code == ErrorCode::AIImageResponseLimitExceeded } + pub fn is_ai_max_required(&self) -> bool { + self.code == ErrorCode::AIMaxRequired + } + static_flowy_error!(internal, ErrorCode::Internal); static_flowy_error!(record_not_found, ErrorCode::RecordNotFound); static_flowy_error!(workspace_initialize, ErrorCode::WorkspaceInitializeError); diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index 76c0185f34..4c4380338b 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -25,6 +25,7 @@ impl From for FlowyError { AppErrorCode::WorkspaceMemberLimitExceeded => ErrorCode::WorkspaceMemberLimitExceeded, AppErrorCode::AIResponseLimitExceeded => ErrorCode::AIResponseLimitExceeded, AppErrorCode::AIImageResponseLimitExceeded => ErrorCode::AIImageResponseLimitExceeded, + AppErrorCode::AIMaxRequired => ErrorCode::AIMaxRequired, AppErrorCode::FileStorageLimitExceeded => ErrorCode::FileStorageLimitExceeded, AppErrorCode::SingleUploadLimitExceeded => ErrorCode::SingleUploadLimitExceeded, AppErrorCode::CustomNamespaceDisabled => ErrorCode::CustomNamespaceRequirePlanUpgrade, From eb508a3ec9955e564452c970592d96d90d49d27f Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Feb 2025 14:05:57 +0800 Subject: [PATCH 006/384] fix: editor stuck on image loading loop when uploading image in row document (#7313) * fix: editor stuck on image loading loop when uploading image in row document * test: editor stuck on image loading loop when uploading image in row document --- .../desktop/cloud/cloud_runner.dart | 4 + .../cloud/database/database_image_test.dart | 80 +++++++++++++++++++ .../cloud/database/database_test_runner.dart | 9 +++ .../image/resizeable_image.dart | 11 ++- 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart create mode 100644 frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart index bc994eba99..a8c05d5f80 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart @@ -1,5 +1,6 @@ import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner; +import 'database/database_test_runner.dart' as database_test_runner; import 'document/document_test_runner.dart' as document_test_runner; import 'set_env.dart' as preset_af_cloud_env_test; import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test; @@ -28,4 +29,7 @@ Future main() async { sidebar_move_page_test.main(); sidebar_rename_untitled_test.main(); sidebar_icon_test.main(); + + // database + database_test_runner.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart new file mode 100644 index 0000000000..5561d40033 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide UploadImageMenu, ResizableImage; +import 'package:easy_localization/easy_localization.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 '../../../shared/constants.dart'; +import '../../../shared/database_test_op.dart'; +import '../../../shared/mock/mock_file_picker.dart'; +import '../../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // copy link to block + group('database image:', () { + testWidgets('insert image', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + // open the first row detail page and upload an image + await tester.createNewPageInSpace( + spaceName: Constants.generalSpaceName, + layout: ViewLayoutPB.Grid, + pageName: 'database image', + ); + await tester.openFirstRowDetailPage(); + + // insert an image block + { + await tester.editor.tapLineOfEditorAt(0); + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_image.tr(), + ); + } + + // upload an image + { + 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 getIt().set(KVKeys.kCloudType, '0'); + 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(); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart new file mode 100644 index 0000000000..4d1a623f07 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart @@ -0,0 +1,9 @@ +import 'package:integration_test/integration_test.dart'; + +import 'database_image_test.dart' as database_image_test; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + database_image_test.main(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 021476aa4e..99524b0d71 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -70,8 +70,12 @@ class _ResizableImageState extends State { @override void initState() { super.initState(); + imageWidth = widget.width; - _userProfilePB = context.read()?.userProfile; + + // read the user profile from the user workspace bloc or the document bloc + _userProfilePB = context.read()?.userProfile ?? + context.read().state.userProfilePB; } @override @@ -97,11 +101,6 @@ class _ResizableImageState extends State { Widget child; final src = widget.src; if (isURL(src)) { - // load network image - if (widget.type == CustomImageType.internal && _userProfilePB == null) { - return _buildLoading(context); - } - _cacheImage = FlowyNetworkImage( url: widget.src, width: imageWidth - moveDistance, From 71a22dc4666180ebc69b9f00567faa6a71e7f903 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:29:56 +0800 Subject: [PATCH 007/384] chore: fix ai page user profile refresh (#7317) --- .../workspace/application/settings/ai/settings_ai_bloc.dart | 4 +++- .../workspace/presentation/settings/settings_dialog.dart | 1 + .../src/af_cloud/impls/user/cloud_service_impl.rs | 3 ++- frontend/rust-lib/flowy-user/src/entities/workspace.rs | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 8a4696606b..b414a71b81 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -109,10 +109,12 @@ class SettingsAIBloc extends Bloc { if (!models.contains(state.selectedAIModel)) { // Use first model as default model if current selected model // is not available + final selectedModel = models[0]; + _updateUserWorkspaceSetting(model: selectedModel); emit( state.copyWith( availableModels: models, - selectedAIModel: models[0], + selectedAIModel: selectedModel, ), ); } else { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 84ba87144b..986e2a4dbd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -141,6 +141,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.ai: if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { return SettingsAIView( + key: ValueKey(user.hashCode), userProfile: user, currentWorkspaceMemberRole: currentWorkspaceMemberRole, workspaceId: workspaceId, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 4f7ce7a3e9..208281fc5f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -18,7 +18,7 @@ use client_api::entity::{ use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; use collab_entity::{CollabObject, CollabType}; -use tracing::instrument; +use tracing::{instrument, trace}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; @@ -586,6 +586,7 @@ where workspace_id: &str, workspace_settings: AFWorkspaceSettingsChange, ) -> Result { + trace!("Sync workspace settings: {:?}", workspace_settings); let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); let client = try_get_client?; diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 240b153e33..885ad6f3cf 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -393,7 +393,7 @@ impl From for UseAISettingPB { } } -#[derive(ProtoBuf, Default, Clone, Validate)] +#[derive(ProtoBuf, Default, Clone, Validate, Debug)] pub struct UpdateUserWorkspaceSettingPB { #[pb(index = 1)] #[validate(custom(function = "required_not_empty_str"))] @@ -410,10 +410,10 @@ impl From for AFWorkspaceSettingsChange { fn from(value: UpdateUserWorkspaceSettingPB) -> Self { let mut change = AFWorkspaceSettingsChange::new(); if let Some(disable_search_indexing) = value.disable_search_indexing { - change = change.disable_search_indexing(disable_search_indexing); + change.disable_search_indexing = Some(disable_search_indexing); } if let Some(ai_model) = value.ai_model { - change = change.ai_model(ai_model); + change.ai_model = Some(ai_model); } change } From f683085618c357e29e566e56b5b881fd9242acc7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Feb 2025 20:57:06 +0800 Subject: [PATCH 008/384] fix: unable to upload file on Android devices (#7314) --- frontend/appflowy_flutter/pubspec.lock | 6 +++--- frontend/appflowy_flutter/pubspec.yaml | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index b3b23666e3..217c708495 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -603,13 +603,13 @@ packages: source: hosted version: "7.0.0" file_picker: - dependency: transitive + dependency: "direct overridden" description: name: file_picker - sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204 + sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c" url: "https://pub.dev" source: hosted - version: "8.1.7" + version: "8.1.4" file_selector_linux: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 2ab3f1b7ef..ec11e4dca7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -197,6 +197,10 @@ dependency_overrides: commit: fbab857b1b1d209240a146d32f496379b9f62276 path: flutter_cache_manager + # Don't upgrade file_picker until the issue is fixed + # https://github.com/miguelpruivo/flutter_file_picker/issues/1652 + file_picker: 8.1.4 + flutter: generate: true uses-material-design: true From 6823fe5d240eac72f3910bf70ad64eefd1ef3cb3 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:40:03 +0800 Subject: [PATCH 009/384] fix: row document images (#7322) * fix: row document images * fix: calendar * chore: fallback to documentbloc --- .../board/presentation/board_page.dart | 12 ++++++---- .../widgets/board_hidden_groups.dart | 12 ++++++---- .../presentation/calendar_event_card.dart | 12 ++++++---- .../calendar/presentation/calendar_page.dart | 22 +++++++++++-------- .../database/grid/presentation/grid_page.dart | 8 +++---- .../widgets/row/relation_row_detail.dart | 12 ++++++---- .../image_menu.dart | 4 +++- .../layouts/image_browser_layout.dart | 3 ++- .../image/resizeable_image.dart | 1 - 9 files changed, 54 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index b373e33b2f..cefcafa03b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; @@ -856,10 +857,13 @@ void _openCard({ FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: databaseController, - rowController: rowController, - userProfile: context.read().userProfile, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + userProfile: context.read().userProfile, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart index c2a90fb49e..c1c127c522 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -426,10 +427,13 @@ class HiddenGroupPopupItemList extends StatelessWidget { onPressed: () { FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: databaseController, - rowController: rowController, - userProfile: context.read().userProfile, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + userProfile: context.read().userProfile, + ), ), ); PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index d4abb79a32..8df96350f9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; @@ -178,10 +179,13 @@ class _EventCardState extends State { FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: widget.databaseController, - rowController: rowController, - userProfile: context.read().userProfile, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: widget.databaseController, + rowController: rowController, + userProfile: context.read().userProfile, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 231c8830d9..8c46cf75a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,6 @@ import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dar import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -390,7 +390,7 @@ void showEventDetails({ context: context, builder: (BuildContext overlayContext) { return BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( rowController: rowController, databaseController: databaseController, @@ -457,14 +457,18 @@ class _UnscheduledEventsButtonState extends State { ), ), ), - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: BlocProvider.value( - value: context.read(), - child: UnscheduleEventsList( - databaseController: widget.databaseController, - unscheduleEvents: state.unscheduleEvents, + popupBuilder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), ), + BlocProvider.value( + value: context.read(), + ), + ], + child: UnscheduleEventsList( + databaseController: widget.databaseController, + unscheduleEvents: state.unscheduleEvents, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 19330221c6..fb32a2868b 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -13,7 +14,6 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -196,7 +196,7 @@ class _GridPageState extends State { FlowyOverlay.show( context: context, builder: (_) => BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( databaseController: context.read().databaseController, rowController: rowController, @@ -233,7 +233,7 @@ class _GridPageState extends State { FlowyOverlay.show( context: context, builder: (_) => BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( databaseController: context.read().databaseController, @@ -559,7 +559,7 @@ class _GridRowsState extends State<_GridRows> { } return BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( rowController: RowController( viewId: viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart index 256de6bc3c..e8d96734dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -26,10 +27,13 @@ class RelatedRowDetailPage extends StatelessWidget { return state.when( loading: () => const SizedBox.shrink(), ready: (databaseController, rowController) { - return RowDetailPage( - databaseController: databaseController, - rowController: rowController, - allowOpenAsFullPage: false, + return BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + allowOpenAsFullPage: false, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index 21caa81297..b07e0b3b08 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; @@ -145,7 +146,8 @@ class _ImageMenuState extends State { showDialog( context: context, builder: (_) => InteractiveImageViewer( - userProfile: context.read().userProfile, + userProfile: context.read()?.userProfile ?? + context.read().state.userProfilePB, imageProvider: AFBlockImageProvider( images: [ ImageBlockData( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart index 1105480bf3..4b53bca127 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart @@ -54,7 +54,8 @@ class _ImageBrowserLayoutState extends State { @override void initState() { super.initState(); - _userProfile = context.read()?.userProfile; + _userProfile = context.read()?.userProfile ?? + context.read().state.userProfilePB; } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 99524b0d71..eb8e8a2707 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -73,7 +73,6 @@ class _ResizableImageState extends State { imageWidth = widget.width; - // read the user profile from the user workspace bloc or the document bloc _userProfilePB = context.read()?.userProfile ?? context.read().state.userProfilePB; } From 43e64d82198bed4fe5a0a7c5f5cd0688a8b3071c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:45:21 +0800 Subject: [PATCH 010/384] test: fix image integration test (#7323) --- .../desktop/document/document_copy_and_paste_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index aa4f8252d5..c18b42939c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -458,7 +458,7 @@ void main() { }); testWidgets('paste the image url', (tester) async { - const plainText = 'https://appflowy.io/1.jpg'; + const plainText = 'http://example.com/1.jpg'; final image = await rootBundle.load('assets/test/images/sample.jpeg'); final bytes = image.buffer.asUint8List(); await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes), From ff2aae213c3de2ad1063e3fe0fede7cfff3766dd Mon Sep 17 00:00:00 2001 From: ArtemisOne <141044353+Artemis1-0@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:53:24 +0000 Subject: [PATCH 011/384] chore(i18n): update es-VE and ga-IE translations (#7320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ckb-KU.json | 4 +- frontend/resources/translations/es-VE.json | 55 +++++++-- frontend/resources/translations/fr-FR.json | 4 +- frontend/resources/translations/ga-IE.json | 119 ++++++++++++++++++++ frontend/resources/translations/id-ID.json | 2 +- frontend/resources/translations/zh-CN.json | 4 +- project.inlang/settings.json | 1 + 7 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 frontend/resources/translations/ga-IE.json diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 4acb7a1765..af7536b951 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -54,8 +54,8 @@ "LogInWithGoogle": "چوونە ژوورەوە لە ڕێگەی گووگڵەوە", "LogInWithGithub": "چوونە ژوورەوە لە ڕێگەی گیتهاب", "LogInWithDiscord": "چوونە ژوورەوە لە ڕێگەی دیسکۆرد", - "loginAsGuestButtonText": "دەست پێ بکە", - "logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە" + "logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە", + "loginAsGuestButtonText": "دەست پێ بکە" }, "workspace": { "chooseWorkspace": "هەڵبژاردنی شوێنی کارەکەت", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 9ecbff9c1d..31174c3dc5 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -36,6 +36,7 @@ "loginButtonText": "Ingresar", "loginStartWithAnonymous": "Comience una sesión anónima", "continueAnonymousUser": "Continuar con una sesión anónima", + "anonymous": "Anónimo", "buttonText": "Ingresar", "signingInText": "Iniciando sesión...", "forgotPassword": "¿Olvidó su contraseña?", @@ -50,6 +51,8 @@ "signInWithGoogle": "Iniciar sesión con Google", "signInWithGithub": "Iniciar sesión con Github", "signInWithDiscord": "Iniciar sesión con Discord", + "signInWithApple": "Continuar con Apple", + "continueAnotherWay": "Continuar por otro camino", "signUpWithGoogle": "Registrarse con Google", "signUpWithGithub": "Registrarse con Github", "signUpWithDiscord": "Registrarse con Discord", @@ -68,8 +71,12 @@ }, "workspace": { "chooseWorkspace": "Elige tu espacio de trabajo", + "defaultName": "Mi espacio de trabajo", "create": "Crear espacio de trabajo", + "new": "Nuevo espacio de trabajo", + "learnMore": "Más información", "reset": "Restablecer espacio de trabajo", + "renameWorkspace": "Cambiar el nombre del espacio de trabajo", "resetWorkspacePrompt": "Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.", "hint": "Espacio de trabajo", "notFoundError": "Espacio de trabajo no encontrado", @@ -105,7 +112,9 @@ "html": "HTML", "clipboard": "Copiar al portapapeles", "csv": "CSV", - "copyLink": "Copiar enlace" + "copyLink": "Copiar enlace", + "publish": "Publicar", + "publishTab": "Publicar" }, "moreAction": { "small": "pequeño", @@ -118,7 +127,9 @@ "charCount": "Número de caracteres : {}", "createdAt": "Creado: {}", "deleteView": "Borrar", - "duplicateView": "Duplicar" + "duplicateView": "Duplicar", + "createdAtLabel": "Creado: ", + "syncedAtLabel": "Sincronizado: " }, "importPanel": { "textAndMarkdown": "Texto y Markdown", @@ -136,7 +147,8 @@ "openNewTab": "Abrir en una pestaña nueva", "moveTo": "Mover a", "addToFavorites": "Añadira los favoritos", - "copyLink": "Copiar Enlace" + "copyLink": "Copiar Enlace", + "move": "Mover" }, "blankPageTitle": "Página en blanco", "newPageText": "Nueva página", @@ -149,16 +161,31 @@ "relatedQuestion": "Relacionado", "serverUnavailable": "Servicio temporalmente no disponible. Por favor, inténtelo de nuevo más tarde.", "aiServerUnavailable": "🌈 ¡Uh-oh! 🌈. Un unicornio se comió nuestra respuesta. ¡Por favor, intenta de nuevo!", + "retry": "Rever", "clickToRetry": "Haga clic para volver a intentarlo", "regenerateAnswer": "Regenerar", "question1": "Cómo utilizar Kanban para gestionar tareas", "question2": "Explica el método GTD", "question3": "¿Por qué usar Rust?", - "aiMistakePrompt": "La IA puede cometer errores. Consulta información importante." + "aiMistakePrompt": "La IA puede cometer errores. Consulta información importante.", + "referenceSource": { + "one": "Se encontró {count} fuente", + "other": "Se encontraron {count} fuentes" + }, + "regenerate": "Intentar otra vez", + "addToNewPage": "Crear nueva página", + "changeFormat": { + "textOnly": "Texto", + "text": "Párrafo" + }, + "selectBanner": { + "saveButton": "Añadir …" + } }, "trash": { "text": "Papelera", "restoreAll": "Recuperar todo", + "restore": "Restaurar", "deleteAll": "Eliminar todo", "pageHeader": { "fileName": "Nombre de archivo", @@ -204,7 +231,8 @@ "moreButtonToolTip": "Eliminar, renombrar y más...", "addPageTooltip": "Inserta una página", "defaultNewPageName": "Sin Título", - "renameDialog": "Renombrar" + "renameDialog": "Renombrar", + "pageNameSuffix": "Copiar" }, "noPagesInside": "No hay páginas dentro", "toolbar": { @@ -260,7 +288,10 @@ "emptyRecent": "Sin documentos recientes", "favoriteSpace": "Favoritos", "RecentSpace": "Reciente", - "Spaces": "Espacios" + "Spaces": "Espacios", + "aiImageResponseLimit": "Se ha quedado sin respuestas de imágenes de IA.\n\nVaya a Configuración -> Plan -> Haga clic en AI Max para obtener más respuestas de imágenes de IA", + "purchaseStorageSpace": "Comprar espacio de almacenamiento", + "purchaseAIResponse": "Compra " }, "notifications": { "export": { @@ -294,6 +325,7 @@ "upload": "Subir", "edit": "Editar", "delete": "Borrar", + "copy": "Copiar", "duplicate": "Duplicar", "putback": "Volver", "update": "Actualizar", @@ -305,6 +337,7 @@ "helpCenter": "Centro de ayuda", "add": "Añadir", "yes": "Si", + "no": "No", "clear": "Limpiar", "remove": "Eliminar", "dontRemove": "no quitar", @@ -319,7 +352,12 @@ "signInDiscord": "Iniciar sesión con discordia", "more": "Más", "create": "Crear", - "close": "Cerca" + "close": "Cerca", + "next": "Próximo", + "previous": "Anterior", + "submit": "Entregar", + "download": "Descargar", + "backToHome": "Volver a Inicio" }, "label": { "welcome": "¡Bienvenido!", @@ -343,6 +381,9 @@ }, "settings": { "title": "Ajustes", + "popupMenuItem": { + "settings": "Ajustes" + }, "accountPage": { "menuLabel": "Mi cuenta", "title": "Mi cuenta", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 326d2d044f..068c3dbade 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -72,8 +72,8 @@ "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", - "loginAsGuestButtonText": "Commencer", - "logInWithMagicLink": "Connectez-vous avec Magic Link" + "logInWithMagicLink": "Connectez-vous avec Magic Link", + "loginAsGuestButtonText": "Commencer" }, "workspace": { "chooseWorkspace": "Choisissez votre espace de travail", diff --git a/frontend/resources/translations/ga-IE.json b/frontend/resources/translations/ga-IE.json new file mode 100644 index 0000000000..1520e46fea --- /dev/null +++ b/frontend/resources/translations/ga-IE.json @@ -0,0 +1,119 @@ +{ + "appName": "Appflowy", + "defaultUsername": "Liom", + "welcomeText": "Fáilte go @:appName", + "welcomeTo": "Fáilte chuig", + "githubStarText": "Réalta ar GitHub", + "subscribeNewsletterText": "Liostáil le Nuachtlitir", + "letsGoButtonText": "Tús Tapa", + "title": "Teideal", + "youCanAlso": "Is féidir leat freisin", + "and": "agus", + "failedToOpenUrl": "Theip ar oscailt an url: {}", + "blockActions": { + "addBelowTooltip": "Cliceáil chun cur leis thíos", + "addAboveCmd": "Alt+cliceáil", + "addAboveMacCmd": "Option+cliceáil", + "addAboveTooltip": "a chur thuas", + "dragTooltip": "Tarraing chun bogadh", + "openMenuTooltip": "Cliceáil chun an roghchlár a oscailt" + }, + "signUp": { + "buttonText": "Cláraigh", + "title": "Cláraigh le @:appName", + "getStartedText": "Faigh Tosaigh", + "emptyPasswordError": "Ní féidir le pasfhocal a bheith folamh", + "repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh", + "unmatchedPasswordError": "Ní ionann pasfhocal athdhéanta agus pasfhocal", + "alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?", + "emailHint": "Ríomhphost", + "passwordHint": "Pasfhocal", + "repeatPasswordHint": "Déan pasfhocal arís", + "signUpWith": "Cláraigh le:" + }, + "signIn": { + "loginTitle": "Logáil isteach ar @:appName", + "loginButtonText": "Logáil isteach", + "loginStartWithAnonymous": "Lean ar aghaidh le seisiún gan ainm", + "continueAnonymousUser": "Lean ar aghaidh le seisiún gan ainm", + "anonymous": "Gan ainm", + "buttonText": "Sínigh Isteach", + "signingInText": "Ag síniú isteach...", + "forgotPassword": "Pasfhocal Dearmadta?", + "emailHint": "Ríomhphost", + "passwordHint": "Pasfhocal", + "dontHaveAnAccount": "Nach bhfuil cuntas agat?", + "createAccount": "Cruthaigh cuntas", + "repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh", + "unmatchedPasswordError": "Ní hionann pasfhocal athdhéanta agus pasfhocal", + "syncPromptMessage": "Seans go dtógfaidh sé tamall na sonraí a shioncronú. Ná dún an leathanach seo, le do thoil", + "or": "NÓ", + "signInWithGoogle": "Lean ar aghaidh le Google", + "signInWithGithub": "Lean ar aghaidh le GitHub", + "signInWithDiscord": "Lean ar aghaidh le Discord", + "signInWithApple": "Lean ar aghaidh le Apple", + "continueAnotherWay": "Lean ar aghaidh ar bhealach eile", + "signUpWithGoogle": "Cláraigh le Google", + "signUpWithGithub": "Cláraigh le Github", + "signUpWithDiscord": "Cláraigh le Discord", + "signInWith": "Lean ar aghaidh le:", + "signInWithEmail": "Lean ar aghaidh le Ríomhphost", + "signInWithMagicLink": "Lean ort", + "signUpWithMagicLink": "Cláraigh le Magic Link", + "pleaseInputYourEmail": "Cuir isteach do sheoladh ríomhphoist", + "settings": "Socruithe", + "magicLinkSent": "Magic Link seolta!", + "invalidEmail": "Cuir isteach seoladh ríomhphoist bailí", + "alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?", + "logIn": "Logáil isteach", + "generalError": "Chuaigh rud éigin mícheart. Bain triail eile as ar ball", + "limitRateError": "Ar chúiseanna slándála, ní féidir leat nasc draíochta a iarraidh ach gach 60 soicind" + }, + "workspace": { + "chooseWorkspace": "Roghnaigh do spás oibre", + "defaultName": "Mo Spás Oibre", + "create": "Cruthaigh spás oibre", + "new": "Spás oibre nua", + "importFromNotion": "Iompórtáil ó Notion", + "learnMore": "Foghlaim níos mó", + "reset": "Athshocraigh spás oibre", + "renameWorkspace": "Athainmnigh spás oibre", + "workspaceNameCannotBeEmpty": "Ní féidir leis an ainm spás oibre a bheith folamh", + "hint": "spás oibre", + "notFoundError": "Spás oibre gan aimsiú", + "errorActions": { + "reportIssue": "Tuairiscigh saincheist", + "reportIssueOnGithub": "Tuairiscigh ceist faoi Github", + "exportLogFiles": "Easpórtáil comhaid logáil", + "reachOut": "Bhaint amach le Discord" + }, + "menuTitle": "Spásanna oibre", + "createSuccess": "Cruthaíodh spás oibre go rathúil", + "leaveCurrentWorkspace": "Fág spás óibre" + }, + "shareAction": { + "buttonText": "Comhroinn", + "workInProgress": "Ag teacht go luath", + "markdown": "Markdown", + "html": "HTML", + "clipboard": "Cóipeáil chuig an ngearrthaisce", + "csv": "CSV", + "copyLink": "Cóipeáil nasc", + "publishToTheWeb": "Foilsigh don Ghréasán", + "publishToTheWebHint": "Cruthaigh suíomh Gréasáin le AppFlowy", + "publish": "Foilsigh", + "unPublish": "Dífhoilsiú", + "visitSite": "Tabhair cuairt ar an suíomh", + "publishTab": "Foilsigh", + "shareTab": "Comhroinn" + }, + "moreAction": { + "small": "beag", + "medium": "meánach", + "large": "mór", + "fontSize": "Clómhéid", + "import": "Iompórtáil", + "createdAt": "Cruthaithe: {}", + "deleteView": "Scrios" + } +} diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index ec87ffabd0..74d1e69d63 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -320,7 +320,7 @@ "ltr": "Kiri ke Kanan", "rtl": "Kanan ke Kiri" }, - "textDirection": { + "textDirection": { "label": "Arah Teks Bawaan", "hint": "Atur arah teks bawaan, apakah dari kiri ke kanan atau kanan ke kiri.", "ltr": "Kiri ke Kanan", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index d7e9b70dac..b3daad9fa2 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -72,8 +72,8 @@ "LogInWithGoogle": "使用 Google 登录", "LogInWithGithub": "使用 Github 登录", "LogInWithDiscord": "使用 Discord 登录", - "loginAsGuestButtonText": "开始使用", - "logInWithMagicLink": "使用魔法链接登录" + "logInWithMagicLink": "使用魔法链接登录", + "loginAsGuestButtonText": "开始使用" }, "workspace": { "chooseWorkspace": "选择您的工作区", diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 394d19473d..1341b40643 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -13,6 +13,7 @@ "fa", "fr-CA", "fr-FR", + "ga-IE", "he", "hu-HU", "id-ID", From 0d89e22ed29477867408bb4b8b4087ae394c12fd Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Thu, 6 Feb 2025 06:54:16 +0100 Subject: [PATCH 012/384] chore(i18n): update ar-SA translations (#7312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index d779063b6b..133f6f6c69 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -384,6 +384,8 @@ "askOwnerToUpgradeToProIOS": "مساحة العمل الخاصة بك على وشك النفاد من مساحة التخزين المجانية.", "askOwnerToUpgradeToAIMax": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بترقية الخطة أو شراء إضافات الذكاء الاصطناعي", "askOwnerToUpgradeToAIMaxIOS": "مساحة العمل الخاصة بك تفتقر إلى الاستجابات المجانية للذكاء الاصطناعي.", + "purchaseAIMax": "لقد نفدت استجابات الصور بالذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بشراء AI Max", + "aiImageResponseLimit": "لقد نفدت استجابات الصور الخاصة بالذكاء الاصطناعي.\nانتقل إلى الإعدادات -> الخطة -> انقر فوق AI Max للحصول على المزيد من استجابات صور AI", "purchaseStorageSpace": "شراء مساحة تخزين", "singleFileProPlanLimitationDescription": "لقد تجاوزت الحد الأقصى لحجم تحميل الملف المسموح به في الخطة المجانية. يرجى الترقية إلى الخطة الاحترافية لتحميل ملفات أكبر حجمًا", "purchaseAIResponse": "شراء", @@ -1194,6 +1196,7 @@ "system": "التكيف مع النظام" }, "fontScaleFactor": "عامل مقياس الخط", + "displaySize": "حجم العرض", "documentSettings": { "cursorColor": "لون مؤشر المستند", "selectionColor": "لون اختيار المستند", @@ -2190,11 +2193,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا" + "clickToOpen": "انقر لفتح السجل" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", From 62a6fb89132bb8140b058d6b361a1e72660438c0 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:10:23 +0800 Subject: [PATCH 013/384] chore: optional response format (#7204) * chore: optional response format * chore: bump client api * chore: code cleanup * chore: bump client api --------- Co-authored-by: Nathan --- frontend/rust-lib/Cargo.lock | 42 +++++++++++++------- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 4 +- frontend/rust-lib/flowy-ai/src/completion.rs | 2 + 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 88d0e9d6f8..9408978e26 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "bytes", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "futures-channel", "futures-util", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "bincode", @@ -1153,7 +1153,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "async-trait", @@ -1400,7 +1400,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1548,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "app-error", @@ -2970,7 +2970,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "futures-util", @@ -2987,7 +2987,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "app-error", @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "bytes", @@ -4624,7 +4624,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4644,6 +4644,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4711,6 +4712,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6140,7 +6154,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 312088862e..b2725230ac 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c80157a687397a38c7390ec3d7fee034b93db963" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c80157a687397a38c7390ec3d7fee034b93db963" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 3c18ee0b9f..12b230580a 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -138,7 +138,7 @@ impl Chat { // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; - let format = params.format.clone().unwrap_or_default().into(); + let format = params.format.clone().map(Into::into).unwrap_or_default(); self.stream_response( params.answer_stream_port, @@ -171,7 +171,7 @@ impl Chat { .store(false, std::sync::atomic::Ordering::SeqCst); self.stream_buffer.lock().await.clear(); - let format = format.unwrap_or_default().into(); + let format = format.map(Into::into).unwrap_or_default(); let answer_stream_buffer = self.stream_buffer.clone(); let uid = self.user_service.user_id()?; diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index fd799b411f..7ce7f79a2e 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -104,8 +104,10 @@ impl CompletionTask { custom_prompt: None, metadata: Some(CompletionMetadata { object_id: self.context.object_id, + workspace_id: None, rag_ids: Some(self.context.rag_ids), }), + format: Default::default(), }; info!("start completion: {:?}", params); From e4b57033b481f4eb32293d3d6b223e5eb212a40d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 6 Feb 2025 22:46:22 +0800 Subject: [PATCH 014/384] fix: improve calendar event button icon color and add confirm dialog (#7330) * fix: improve calendar event button icon color and add confirm dialog * test: update test --- .../shared/database_test_op.dart | 1 + .../presentation/calendar_event_editor.dart | 42 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 676c96f042..5f27305fe5 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1464,6 +1464,7 @@ extension AppFlowyDatabaseTest on WidgetTester { ); await tapButton(button); + await tapButtonWithName(LocaleKeys.button_delete.tr()); } Future dragDropRescheduleCalendarEvent() async { diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index 40f57b5e9a..576e5d198c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -12,6 +12,7 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -92,11 +93,11 @@ class EventEditorControls extends StatelessWidget { message: LocaleKeys.calendar_duplicateEvent.tr(), child: FlowyIconButton( width: 20, - icon: const FlowySvg( + icon: FlowySvg( FlowySvgs.m_duplicate_s, - size: Size.square(17), + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, ), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, onPressed: () => context.read().add( CalendarEvent.duplicateEvent( rowController.viewId, @@ -108,20 +109,35 @@ class EventEditorControls extends StatelessWidget { const HSpace(8.0), FlowyIconButton( width: 20, - icon: const FlowySvg(FlowySvgs.delete_s), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, - onPressed: () => context.read().add( - CalendarEvent.deleteEvent( - rowController.viewId, - rowController.rowId, - ), - ), + icon: FlowySvg( + FlowySvgs.delete_s, + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + showConfirmDeletionDialog( + context: context, + name: LocaleKeys.grid_row_label.tr(), + description: LocaleKeys.grid_row_deleteRowPrompt.tr(), + onConfirm: () { + context.read().add( + CalendarEvent.deleteEvent( + rowController.viewId, + rowController.rowId, + ), + ); + }, + ); + }, ), const HSpace(8.0), FlowyIconButton( width: 20, - icon: const FlowySvg(FlowySvgs.full_view_s), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + icon: FlowySvg( + FlowySvgs.full_view_s, + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, + ), onPressed: () { PopoverContainer.of(context).close(); onExpand.call(); From 8b1a03713b146e97475fc7bf051935e26922f4aa Mon Sep 17 00:00:00 2001 From: hasanbeder Date: Fri, 7 Feb 2025 04:58:15 +0300 Subject: [PATCH 015/384] feat(i18n): Add Turkish (tr-TR) language translation (#7329) - Complete Turkish language translation for AppFlowy - Covers all UI elements and user-facing strings - Improves localization support for Turkish users --- frontend/resources/translations/tr-TR.json | 2285 +++++++++++++------- 1 file changed, 1536 insertions(+), 749 deletions(-) diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 873f4cf355..d2d0c9eea6 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1,98 +1,100 @@ { "appName": "AppFlowy", - "defaultUsername": "Ben", - "welcomeText": "@:appName'a Hoş Geldiniz", + "defaultUsername": "Kullanıcı", + "welcomeText": "@:appName'ye Hoş Geldiniz", "welcomeTo": "Hoş Geldiniz", - "githubStarText": "GitHub'da Yıldız Verin", - "subscribeNewsletterText": "Bültene Abone Olun", - "letsGoButtonText": "Hızlı Başlangıç", + "githubStarText": "GitHub'da Yıldız Ver", + "subscribeNewsletterText": "Bültenimize Abone Ol", + "letsGoButtonText": "Hemen Başla", "title": "Başlık", - "youCanAlso": "Ayrıca şunları da yapabilirsiniz", + "youCanAlso": "Ayrıca", "and": "ve", "failedToOpenUrl": "URL açılamadı: {}", "blockActions": { - "addBelowTooltip": "Alta eklemek için tıklayın", - "addAboveCmd": "Alt+tıkla", - "addAboveMacCmd": "Option+tıkla", - "addAboveTooltip": "üste eklemek için", - "dragTooltip": "Taşımak için sürükleyin", - "openMenuTooltip": "Menüyü açmak için tıklayın" + "addBelowTooltip": "Altına eklemek için tıklayın", + "addAboveCmd": "Alt+tıklama", + "addAboveMacCmd": "Option+tıklama", + "addAboveTooltip": "Üstüne eklemek için", + "dragTooltip": "Sürükleyerek taşıyın", + "openMenuTooltip": "Menüyü aç" }, "signUp": { "buttonText": "Kayıt Ol", - "title": "@:appName'a Kaydolun", + "title": "@:appName'e Kayıt Ol", "getStartedText": "Başlayın", - "emptyPasswordError": "Parola boş bırakılamaz", - "repeatPasswordEmptyError": "Parola tekrarı boş bırakılamaz", - "unmatchedPasswordError": "Parola tekrarı, girdiğiniz parolayla aynı değil", - "alreadyHaveAnAccount": "Zaten bir hesabınız var mı?", - "emailHint": "E-posta", + "emptyPasswordError": "Parola boş olamaz", + "repeatPasswordEmptyError": "Parola tekrarı alanı boş olamaz", + "unmatchedPasswordError": "Parola tekrarı, parola ile aynı değil", + "alreadyHaveAnAccount": "Hesabınız zaten var mı?", + "emailHint": "E-posta adresi", "passwordHint": "Parola", - "repeatPasswordHint": "Parolayı tekrar girin", - "signUpWith": "Şununla kaydolun:" + "repeatPasswordHint": "Parolayı tekrarla", + "signUpWith": "Kayıt ol:" }, "signIn": { - "loginTitle": "@:appName'a Giriş Yapın", - "loginButtonText": "Giriş Yap", - "loginStartWithAnonymous": "Anonim oturumla devam et", + "loginTitle": "@:appName'e Oturum Aç", + "loginButtonText": "Oturum Aç", + "loginStartWithAnonymous": "Anonim oturumla başla", "continueAnonymousUser": "Anonim oturumla devam et", "anonymous": "Anonim", - "buttonText": "Giriş Yap", - "signingInText": "Giriş yapılıyor...", - "forgotPassword": "Parolanızı mı unuttunuz?", - "emailHint": "E-posta", + "buttonText": "Oturum Aç", + "signingInText": "Oturum açılıyor...", + "forgotPassword": "Parolamı Unuttum?", + "emailHint": "E-posta adresi", "passwordHint": "Parola", "dontHaveAnAccount": "Hesabınız yok mu?", "createAccount": "Hesap oluştur", - "repeatPasswordEmptyError": "Parola tekrarı boş bırakılamaz", - "unmatchedPasswordError": "Parola tekrarı, girdiğiniz parolayla aynı değil", - "syncPromptMessage": "Veriler senkronize ediliyor, lütfen bu sayfayı kapatmayın", + "repeatPasswordEmptyError": "Parola tekrarı alanı boş bırakılamaz", + "unmatchedPasswordError": "Parola tekrarı parolayla eşleşmiyor", + "syncPromptMessage": "Verilerin senkronize edilmesi biraz zaman alabilir. Lütfen bu sayfayı kapatmayın", "or": "VEYA", - "signInWithGoogle": "Google ile Giriş Yap", - "signInWithGithub": "Github ile Giriş Yap", - "signInWithDiscord": "Discord ile Giriş Yap", + "signInWithGoogle": "Google ile devam et", + "signInWithGithub": "GitHub ile devam et", + "signInWithDiscord": "Discord ile devam et", "signInWithApple": "Apple ile devam et", - "signUpWithGoogle": "Google ile Kaydol", - "signUpWithGithub": "Github ile Kaydol", - "signUpWithDiscord": "Discord ile Kaydol", - "signInWith": "Şununla giriş yapın:", - "signInWithEmail": "E-posta ile giriş yap", - "signInWithMagicLink": "Devam Et", - "signUpWithMagicLink": "Sihirli Bağlantı ile Kaydol", + "continueAnotherWay": "Başka yöntemle devam et", + "signUpWithGoogle": "Google ile kaydol", + "signUpWithGithub": "GitHub ile kaydol", + "signUpWithDiscord": "Discord ile kaydol", + "signInWith": "Devam et:", + "signInWithEmail": "E-posta ile devam et", + "signInWithMagicLink": "Devam et", + "signUpWithMagicLink": "Sihirli Bağlantı ile kaydol", "pleaseInputYourEmail": "Lütfen e-posta adresinizi girin", "settings": "Ayarlar", - "magicLinkSent": "Sihirli bağlantı e-postanıza gönderildi!", - "invalidEmail": "Lütfen geçerli bir e-posta adresi girin", - "alreadyHaveAnAccount": "Zaten bir hesabınız var mı?", - "logIn": "Giriş Yap", - "generalError": "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyin", - "limitRateError": "Güvenlik nedeniyle, sihirli bağlantı talebi 60 saniyede bir yapılabilir", - "magicLinkSentDescription": "E-postanıza bir Sihirli Bağlantı gönderildi. Girişinizi tamamlamak için bağlantıya tıklayın. Bağlantı 5 dakika sonra sona erecek." + "magicLinkSent": "Sihirli Bağlantı gönderildi!", + "invalidEmail": "Geçerli bir e-posta adresi girin", + "alreadyHaveAnAccount": "Zaten hesabınız var mı?", + "logIn": "Oturum Aç", + "generalError": "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.", + "limitRateError": "Güvenlik önlemi olarak, sihirli bağlantı talepleri 60 saniyede bir ile sınırlandırılmıştır.", + "magicLinkSentDescription": "E-posta adresinize sihirli bir bağlantı gönderdik. Giriş yapmak için bu bağlantıya tıklayın. Bağlantı 5 dakika içinde geçersiz hale gelecektir." }, "workspace": { "chooseWorkspace": "Çalışma alanınızı seçin", "defaultName": "Çalışma Alanım", - "create": "Çalışma Alanı Oluştur", - "importFromNotion": "Notion'dan İçe Aktar", - "learnMore": "Daha fazla bilgi edin", - "reset": "Çalışma Alanını Sıfırla", - "renameWorkspace": "Çalışma Alanını Yeniden Adlandır", + "create": "Çalışma alanı oluştur", + "new": "Yeni çalışma alanı", + "importFromNotion": "Notion'dan içe aktar", + "learnMore": "Daha fazla bilgi", + "reset": "Çalışma alanını sıfırla", + "renameWorkspace": "Çalışma alanını yeniden adlandır", "workspaceNameCannotBeEmpty": "Çalışma alanı adı boş olamaz", - "resetWorkspacePrompt": "Çalışma alanını sıfırlamak, içindeki tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Alternatif olarak, çalışma alanını geri yüklemek için destek ekibiyle iletişime geçebilirsiniz", + "resetWorkspacePrompt": "Çalışma alanını sıfırlamak tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Geri yüklemek için destek ekibine ulaşabilirsiniz.", "hint": "çalışma alanı", "notFoundError": "Çalışma alanı bulunamadı", - "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm @:appName örneklerini kapatıp tekrar deneyin.", + "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. @:appName'in açık olan tüm örneklerini kapatıp tekrar deneyin.", "errorActions": { - "reportIssue": "Sorun bildir", - "reportIssueOnGithub": "GitHub'da sorun bildir", + "reportIssue": "Hata bildir", + "reportIssueOnGithub": "GitHub'da hata bildir", "exportLogFiles": "Günlük dosyalarını dışa aktar", - "reachOut": "Discord'da ulaşın" + "reachOut": "Discord'da iletişime geç" }, "menuTitle": "Çalışma Alanları", - "deleteWorkspaceHintText": "Çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız sayfalar da silinecektir.", + "deleteWorkspaceHintText": "Çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır.", "createSuccess": "Çalışma alanı başarıyla oluşturuldu", "createFailed": "Çalışma alanı oluşturulamadı", - "createLimitExceeded": "Hesabınız için izin verilen maksimum çalışma alanı sınırına ulaştınız. Çalışmanıza devam etmek için ek çalışma alanlarına ihtiyacınız varsa, lütfen GitHub'da talepte bulunun", + "createLimitExceeded": "Hesabınız için izin verilen maksimum çalışma alanı limitine ulaştınız. Çalışmanıza devam etmek için ek çalışma alanlarına ihtiyacınız varsa, lütfen GitHub'da talepte bulunun", "deleteSuccess": "Çalışma alanı başarıyla silindi", "deleteFailed": "Çalışma alanı silinemedi", "openSuccess": "Çalışma alanı başarıyla açıldı", @@ -103,85 +105,147 @@ "updateIconFailed": "Çalışma alanı simgesi güncellenemedi", "cannotDeleteTheOnlyWorkspace": "Tek çalışma alanı silinemez", "fetchWorkspacesFailed": "Çalışma alanları getirilemedi", - "leaveCurrentWorkspace": "Çalışma alanından çık", - "leaveCurrentWorkspacePrompt": "Geçerli çalışma alanından çıkmak istediğinizden emin misiniz?" + "leaveCurrentWorkspace": "Çalışma alanından ayrıl", + "leaveCurrentWorkspacePrompt": "Mevcut çalışma alanından ayrılmak istediğinizden emin misiniz?" }, "shareAction": { "buttonText": "Paylaş", - "workInProgress": "Yakında geliyor", + "workInProgress": "Yakında", "markdown": "Markdown", "html": "HTML", "clipboard": "Panoya kopyala", "csv": "CSV", - "copyLink": "Bağlantıyı Kopyala", + "copyLink": "Bağlantıyı kopyala", "publishToTheWeb": "Web'de Yayınla", "publishToTheWebHint": "AppFlowy ile bir web sitesi oluşturun", "publish": "Yayınla", "unPublish": "Yayından kaldır", "visitSite": "Siteyi ziyaret et", - "exportAsTab": "Şu şekilde dışa aktar", + "exportAsTab": "Farklı dışa aktar", "publishTab": "Yayınla", "shareTab": "Paylaş", - "publishOnAppFlowy": "AppFlowy'de yayınla", + "publishOnAppFlowy": "AppFlowy'de Yayınla", + "shareTabTitle": "İşbirliği için davet et", + "shareTabDescription": "Herhangi biriyle kolay işbirliği için", "copyLinkSuccess": "Bağlantı panoya kopyalandı", - "copyLinkFailed": "Bağlantı panoya kopyalanamadı" + "copyShareLink": "Paylaşım bağlantısını kopyala", + "copyLinkFailed": "Bağlantı panoya kopyalanamadı", + "copyLinkToBlockSuccess": "Blok bağlantısı panoya kopyalandı", + "copyLinkToBlockFailed": "Blok bağlantısı panoya kopyalanamadı", + "manageAllSites": "Tüm siteleri yönet", + "updatePathName": "Yol adını güncelle" }, "moreAction": { "small": "küçük", "medium": "orta", "large": "büyük", - "fontSize": "Yazı boyutu", - "import": "İçe Aktar", + "fontSize": "Yazı tipi boyutu", + "import": "İçe aktar", "moreOptions": "Daha fazla seçenek", "wordCount": "Kelime sayısı: {}", "charCount": "Karakter sayısı: {}", - "createdAt": "Oluşturulma tarihi: {}", + "createdAt": "Oluşturulma: {}", "deleteView": "Sil", - "duplicateView": "Kopyala" + "duplicateView": "Çoğalt", + "wordCountLabel": "Kelime sayısı: ", + "charCountLabel": "Karakter sayısı: ", + "createdAtLabel": "Oluşturulma: ", + "syncedAtLabel": "Senkronize edilme: ", + "saveAsNewPage": "Mesajları sayfaya ekle" }, "importPanel": { - "textAndMarkdown": "Metin & Markdown", - "documentFromV010": "v0.1.0 Belgesi", - "databaseFromV010": "v0.1.0 Veritabanı", + "textAndMarkdown": "Metin ve Markdown", + "documentFromV010": "v0.1.0'dan belge", + "databaseFromV010": "v0.1.0'dan veritabanı", + "notionZip": "Notion Dışa Aktarılmış Zip Dosyası", "csv": "CSV", "database": "Veritabanı" }, "disclosureAction": { - "rename": "Yeniden Adlandır", + "rename": "Yeniden adlandır", "delete": "Sil", - "duplicate": "Kopyala", - "unfavorite": "Favorilerden çıkar", + "duplicate": "Çoğalt", + "unfavorite": "Favorilerden kaldır", "favorite": "Favorilere ekle", "openNewTab": "Yeni sekmede aç", - "moveTo": "Taşı", - "addToFavorites": "Favorilere Ekle", - "copyLink": "Bağlantıyı Kopyala", - "changeIcon": "Simgeyi Değiştir", - "collapseAllPages": "Tüm alt sayfaları daralt" + "moveTo": "Şuraya taşı", + "addToFavorites": "Favorilere ekle", + "copyLink": "Bağlantıyı kopyala", + "changeIcon": "Simgeyi değiştir", + "collapseAllPages": "Tüm alt sayfaları daralt", + "movePageTo": "Sayfayı şuraya taşı", + "move": "Taşı" }, "blankPageTitle": "Boş sayfa", "newPageText": "Yeni sayfa", "newDocumentText": "Yeni belge", - "newGridText": "Yeni Kılavuz", + "newGridText": "Yeni ızgara", "newCalendarText": "Yeni takvim", "newBoardText": "Yeni pano", "chat": { "newChat": "Yapay Zeka Sohbeti", - "inputMessageHint": "AppFlowy Yapay Zekasına mesaj gönder", - "unsupportedCloudPrompt": "Bu özellik yalnızca AppFlowy Cloud kullanıldığında kullanılabilir", - "relatedQuestion": "İlgili", - "serverUnavailable": "Hizmet geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin.", - "aiServerUnavailable": "🌈 Üzgünüz! 🌈. Bir tek boynuzlu at yanıtımızı yedi. Lütfen tekrar deneyin!", - "clickToRetry": "Yeniden denemek için tıklayın", + "inputMessageHint": "@:appName Yapay Zekasına sorun", + "inputLocalAIMessageHint": "@:appName Yerel Yapay Zekasına sorun", + "unsupportedCloudPrompt": "Bu özellik yalnızca @:appName Cloud kullanırken kullanılabilir", + "relatedQuestion": "Önerilen", + "serverUnavailable": "Bağlantı kesildi. Lütfen internet bağlantınızı kontrol edin ve", + "aiServerUnavailable": "Yapay zeka hizmeti geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin.", + "retry": "Tekrar dene", + "clickToRetry": "Tekrar denemek için tıklayın", "regenerateAnswer": "Yeniden oluştur", - "question1": "Kanban'ı kullanarak görevleri yönetme", + "question1": "Görevleri yönetmek için Kanban nasıl kullanılır", "question2": "GTD yöntemini açıkla", - "question3": "Neden Rust kullanmalıyım?", - "question4": "Mutfağımdakilerle tarif", + "question3": "Neden Rust kullanmalı", + "question4": "Mutfağımdaki malzemelerle tarif", + "question5": "Sayfam için bir illüstrasyon oluştur", + "question6": "Önümüzdeki hafta için yapılacaklar listesi hazırla", "aiMistakePrompt": "Yapay zeka hata yapabilir. Önemli bilgileri kontrol edin.", - "chatWithFilePrompt": "Dosyayla sohbet etmek istiyor musunuz?", - "indexFileSuccess": "Dosyayı başarıyla indeksleme", - "indexingFile": "{} indeksleniyor" + "chatWithFilePrompt": "Dosya ile sohbet etmek ister misiniz?", + "indexFileSuccess": "Dosya başarıyla indekslendi", + "inputActionNoPages": "Sayfa sonucu yok", + "referenceSource": { + "zero": "0 kaynak bulundu", + "one": "{count} kaynak bulundu", + "other": "{count} kaynak bulundu" + }, + "clickToMention": "Bir sayfadan bahset", + "uploadFile": "PDF, metin veya markdown dosyaları ekle", + "questionDetail": "Merhaba {}! Size bugün nasıl yardımcı olabilirim?", + "indexingFile": "{} indeksleniyor", + "generatingResponse": "Yanıt oluşturuluyor", + "selectSources": "Kaynakları Seç", + "sourcesLimitReached": "En fazla 3 üst düzey belge ve alt öğelerini seçebilirsiniz", + "sourceUnsupported": "Şu anda veritabanlarıyla sohbet etmeyi desteklemiyoruz", + "regenerate": "Tekrar dene", + "addToPageButton": "Mesajı sayfaya ekle", + "addToPageTitle": "Mesajı şuraya ekle...", + "addToNewPage": "Yeni sayfa oluştur", + "addToNewPageName": "\"{}\" kaynağından çıkarılan mesajlar", + "addToNewPageSuccessToast": "Mesaj şuraya eklendi:", + "openPagePreviewFailedToast": "Sayfa açılamadı", + "changeFormat": { + "actionButton": "Biçimi değiştir", + "confirmButton": "Bu biçimle yeniden oluştur", + "textOnly": "Metin", + "imageOnly": "Sadece görsel", + "textAndImage": "Metin ve Görsel", + "text": "Paragraf", + "bullet": "Madde işaretli liste", + "number": "Numaralı liste", + "table": "Tablo", + "blankDescription": "Yanıt biçimi", + "defaultDescription": "Otomatik mod", + "textWithImageDescription": "@:chat.changeFormat.text ve görsel", + "numberWithImageDescription": "@:chat.changeFormat.number ve görsel", + "bulletWithImageDescription": "@:chat.changeFormat.bullet ve görsel", + "tableWithImageDescription": "@:chat.changeFormat.table ve görsel" + }, + "selectBanner": { + "saveButton": "Şuraya ekle …", + "selectMessages": "Mesajları seç", + "nSelected": "{} seçildi", + "allSelected": "Tümü seçildi" + } }, "trash": { "text": "Çöp Kutusu", @@ -194,11 +258,11 @@ "created": "Oluşturulma" }, "confirmDeleteAll": { - "title": "Çöp Kutusu'ndaki tüm sayfaları silmek istediğinizden emin misiniz?", - "caption": "Bu işlem geri alınamaz." + "title": "Çöp kutusundaki tüm sayfalar", + "caption": "Çöp kutusundaki her şeyi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." }, "confirmRestoreAll": { - "title": "Çöp Kutusu'ndaki tüm sayfaları geri yüklemek istediğinizden emin misiniz?", + "title": "Çöp kutusundaki tüm sayfaları geri yükle", "caption": "Bu işlem geri alınamaz." }, "restorePage": { @@ -207,15 +271,15 @@ }, "mobile": { "actions": "Çöp Kutusu İşlemleri", - "empty": "Çöp Kutusu Boş", - "emptyDescription": "Silinmiş dosyanız yok", + "empty": "Çöp kutusunda sayfa veya alan yok", + "emptyDescription": "İhtiyacınız olmayan şeyleri Çöp Kutusuna taşıyın.", "isDeleted": "silindi", "isRestored": "geri yüklendi" }, "confirmDeleteTitle": "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz?" }, "deletePagePrompt": { - "text": "Bu sayfa Çöp Kutusu'nda", + "text": "Bu sayfa Çöp Kutusunda", "restore": "Sayfayı geri yükle", "deletePermanent": "Kalıcı olarak sil", "deletePermanentDescription": "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." @@ -223,8 +287,8 @@ "dialogCreatePageNameHint": "Sayfa adı", "questionBubble": { "shortcuts": "Kısayollar", - "whatsNew": "Yenilikler?", - "help": "Yardım & Destek", + "whatsNew": "Yenilikler", + "help": "Yardım ve Destek", "markdown": "Markdown", "debug": { "name": "Hata Ayıklama Bilgisi", @@ -235,20 +299,21 @@ }, "menuAppHeader": { "moreButtonToolTip": "Kaldır, yeniden adlandır ve daha fazlası...", - "addPageTooltip": "İçine hızlıca bir sayfa ekleyin", - "defaultNewPageName": "İsimsiz", - "renameDialog": "Yeniden Adlandır" + "addPageTooltip": "Hızlıca içeri sayfa ekle", + "defaultNewPageName": "Başlıksız", + "renameDialog": "Yeniden adlandır", + "pageNameSuffix": "Kopya" }, - "noPagesInside": "İçinde sayfa yok", + "noPagesInside": "İçeride sayfa yok", "toolbar": { - "undo": "Geri Al", + "undo": "Geri al", "redo": "Yinele", "bold": "Kalın", "italic": "İtalik", - "underline": "Altı Çizili", - "strike": "Üstü Çizili", - "numList": "Numaralı Liste", - "bulletList": "Madde İşaretli Liste", + "underline": "Altı çizili", + "strike": "Üstü çizili", + "numList": "Numaralı liste", + "bulletList": "Madde işaretli liste", "checkList": "Kontrol Listesi", "inlineCode": "Satır İçi Kod", "quote": "Alıntı Bloğu", @@ -259,64 +324,75 @@ "link": "Bağlantı" }, "tooltip": { - "lightMode": "Açık moda geç", - "darkMode": "Koyu moda geç", + "lightMode": "Aydınlık moda geç", + "darkMode": "Karanlık moda geç", "openAsPage": "Sayfa olarak aç", - "addNewRow": "Yeni bir satır ekleyin", + "addNewRow": "Yeni satır ekle", "openMenu": "Menüyü açmak için tıklayın", - "dragRow": "Satırı yeniden sıralamak için uzun basın", + "dragRow": "Satırı yeniden sıralamak için sürükleyin", "viewDataBase": "Veritabanını görüntüle", - "referencePage": "Bu {name} referans gösteriliyor", - "addBlockBelow": "Alta bir blok ekle", + "referencePage": "Bu {name} referans alındı", + "addBlockBelow": "Alta blok ekle", "aiGenerate": "Oluştur" }, "sideBar": { "closeSidebar": "Kenar çubuğunu kapat", "openSidebar": "Kenar çubuğunu aç", + "expandSidebar": "Tam sayfa olarak genişlet", "personal": "Kişisel", "private": "Özel", "workspace": "Çalışma Alanı", "favorites": "Favoriler", - "clickToHidePrivate": "Özel alanı gizle\nBurada oluşturduğunuz sayfalar yalnızca size görünür", - "clickToHideWorkspace": "Çalışma alanını gizle\nBurada oluşturduğunuz sayfalar tüm üyelere görünür", - "clickToHidePersonal": "Kişisel alanı gizle", - "clickToHideFavorites": "Favori alanı gizle", - "addAPage": "Sayfa ekle", + "clickToHidePrivate": "Özel alanı gizlemek için tıklayın\nBurada oluşturduğunuz sayfalar yalnızca size görünür", + "clickToHideWorkspace": "Çalışma alanını gizlemek için tıklayın\nBurada oluşturduğunuz sayfalar tüm üyelere görünür", + "clickToHidePersonal": "Kişisel alanı gizlemek için tıklayın", + "clickToHideFavorites": "Favoriler alanını gizlemek için tıklayın", + "addAPage": "Yeni sayfa ekle", "addAPageToPrivate": "Özel alana sayfa ekle", "addAPageToWorkspace": "Çalışma alanına sayfa ekle", "recent": "Son", "today": "Bugün", "thisWeek": "Bu hafta", "others": "Önceki favoriler", + "earlier": "Daha önce", "justNow": "az önce", "minutesAgo": "{count} dakika önce", - "lastViewed": "Son görüntülenme", - "favoriteAt": "Favorilere eklenme", - "emptyRecent": "Son Belge Yok", - "emptyRecentDescription": "Belgeleri görüntüledikçe, kolayca erişim için burada görünürler", - "emptyFavorite": "Favori Belge Yok", - "emptyFavoriteDescription": "Keşfetmeye başlayın ve belgeleri favori olarak işaretleyin. Hızlı erişim için burada listelenirler!", - "removePageFromRecent": "Bu sayfayı Son Kullanılanlar'dan kaldırmak ister misiniz?", + "lastViewed": "Son görüntüleme", + "favoriteAt": "Favorilere eklendi", + "emptyRecent": "Son Sayfa Yok", + "emptyRecentDescription": "Sayfaları görüntüledikçe, kolay erişim için burada listelenecekler.", + "emptyFavorite": "Favori Sayfa Yok", + "emptyFavoriteDescription": "Sayfaları favori olarak işaretleyin—hızlı erişim için burada listelenecekler!", + "removePageFromRecent": "Bu sayfayı Son'dan kaldır?", "removeSuccess": "Başarıyla kaldırıldı", "favoriteSpace": "Favoriler", "RecentSpace": "Son", "Spaces": "Alanlar", - "upgradeToPro": "Pro'ya Yükselt", - "upgradeToAIMax": "Sınırsız Yapay Zeka'nın Kilidini Aç", - "storageLimitDialogTitle": "Ücretsiz depolama alanınız tükendi. Sınırsız depolama alanının kilidini açmak için yükseltin", - "aiResponseLimitTitle": "Ücretsiz Yapay Zeka yanıtlarınız tükendi. Sınırsız yanıtların kilidini açmak için Pro Planına yükseltin veya bir Yapay Zeka eklentisi satın alın", - "aiResponseLimitDialogTitle": "Yapay Zeka Yanıtları sınırı aşıldı", - "aiResponseLimit": "Ücretsiz Yapay Zeka yanıtlarınız tükendi.\n\nDaha fazla Yapay Zeka yanıtı almak için Ayarlar -> Plan -> AI Max veya Pro Planına tıklayın", - "askOwnerToUpgradeToPro": "Çalışma alanınızın ücretsiz depolama alanı tükeniyor. Lütfen çalışma alanı sahibinden Pro Planına yükseltmesini isteyin", - "askOwnerToUpgradeToAIMax": "Çalışma alanınızın ücretsiz Yapay Zeka yanıtları tükeniyor. Lütfen çalışma alanı sahibinden planı yükseltmesini veya Yapay Zeka eklentileri satın almasını isteyin", + "upgradeToPro": "Pro'ya yükselt", + "upgradeToAIMax": "Sınırsız yapay zekayı aç", + "storageLimitDialogTitle": "Ücretsiz depolama alanınız bitti. Sınırsız depolama için yükseltin", + "storageLimitDialogTitleIOS": "Ücretsiz depolama alanınız bitti.", + "aiResponseLimitTitle": "Ücretsiz yapay zeka yanıtlarınız bitti. Sınırsız yanıt için Pro Plana yükseltin veya bir yapay zeka eklentisi satın alın", + "aiResponseLimitTitleIOS": "Ücretsiz yapay zeka yanıtlarınız bitti.", + "aiResponseLimitDialogTitle": "Yapay zeka yanıt limitine ulaşıldı", + "aiResponseLimit": "Ücretsiz yapay zeka yanıtlarınız bitti.\n\nDaha fazla yapay zeka yanıtı almak için Ayarlar -> Plan -> AI Max veya Pro Plan'a tıklayın", + "askOwnerToUpgradeToPro": "Çalışma alanınızın ücretsiz depolama alanı bitiyor. Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin", + "askOwnerToUpgradeToProIOS": "Çalışma alanınızın ücretsiz depolama alanı bitiyor.", + "askOwnerToUpgradeToAIMax": "Çalışma alanınızın ücretsiz yapay zeka yanıtları bitti. Lütfen çalışma alanı sahibinden planı yükseltmesini veya yapay zeka eklentileri satın almasını isteyin", + "askOwnerToUpgradeToAIMaxIOS": "Çalışma alanınızın ücretsiz yapay zeka yanıtları bitiyor.", + "purchaseAIMax": "Çalışma alanınızın yapay zeka görsel yanıtları bitti. Lütfen çalışma alanı sahibinden AI Max satın almasını isteyin", + "aiImageResponseLimit": "Yapay zeka görsel yanıtlarınız bitti.\n\nDaha fazla yapay zeka görsel yanıtı almak için Ayarlar -> Plan -> AI Max'a tıklayın", "purchaseStorageSpace": "Depolama Alanı Satın Al", - "purchaseAIResponse": "Satın Al", - "upgradeToAILocal": "Cihazınızda çevrimdışı Yapay Zeka" + "singleFileProPlanLimitationDescription": "Ücretsiz planda izin verilen maksimum dosya yükleme boyutunu aştınız. Daha büyük dosyalar yüklemek için lütfen Pro Plana yükseltin", + "purchaseAIResponse": "Satın Al ", + "askOwnerToUpgradeToLocalAI": "Çalışma alanı sahibinden Cihaz Üzerinde Yapay Zekayı etkinleştirmesini isteyin", + "upgradeToAILocal": "En üst düzey gizlilik için yerel modelleri cihazınızda çalıştırın", + "upgradeToAILocalDesc": "Yerel yapay zeka kullanarak PDF'lerle sohbet edin, yazılarınızı geliştirin ve tabloları otomatik doldurun" }, "notifications": { "export": { "markdown": "Not Markdown Olarak Dışa Aktarıldı", - "path": "Belgeler/flowy" + "path": "Documents/flowy" } }, "contactsPage": { @@ -337,25 +413,26 @@ "generate": "Oluştur", "esc": "ESC", "keep": "Sakla", - "tryAgain": "Tekrar Dene", + "tryAgain": "Tekrar dene", "discard": "Vazgeç", "replace": "Değiştir", - "insertBelow": "Alta Ekle", - "insertAbove": "Üste Ekle", + "insertBelow": "Alta ekle", + "insertAbove": "Üste ekle", "upload": "Yükle", "edit": "Düzenle", "delete": "Sil", - "duplicate": "Kopyala", + "copy": "Kopyala", + "duplicate": "Çoğalt", "putback": "Geri Koy", "update": "Güncelle", "share": "Paylaş", - "removeFromFavorites": "Favorilerden çıkar", - "removeFromRecent": "Son Kullanılanlar'dan kaldır", + "removeFromFavorites": "Favorilerden kaldır", + "removeFromRecent": "Son'dan kaldır", "addToFavorites": "Favorilere ekle", "favoriteSuccessfully": "Favorilere eklendi", - "unfavoriteSuccessfully": "Favorilerden çıkarıldı", - "duplicateSuccessfully": "Başarıyla kopyalandı", - "rename": "Yeniden Adlandır", + "unfavoriteSuccessfully": "Favorilerden kaldırıldı", + "duplicateSuccessfully": "Başarıyla çoğaltıldı", + "rename": "Yeniden adlandır", "helpCenter": "Yardım Merkezi", "add": "Ekle", "yes": "Evet", @@ -365,50 +442,118 @@ "dontRemove": "Kaldırma", "copyLink": "Bağlantıyı Kopyala", "align": "Hizala", - "login": "Giriş Yap", - "logout": "Çıkış Yap", - "deleteAccount": "Hesabı Sil", + "login": "Giriş yap", + "logout": "Çıkış yap", + "deleteAccount": "Hesabı sil", "back": "Geri", - "signInGoogle": "Google ile Giriş Yap", - "signInGithub": "Github ile Giriş Yap", - "signInDiscord": "Discord ile Giriş Yap", - "more": "Daha Fazla", + "signInGoogle": "Google ile devam et", + "signInGithub": "GitHub ile devam et", + "signInDiscord": "Discord ile devam et", + "more": "Daha fazla", "create": "Oluştur", "close": "Kapat", - "next": "Sonraki", - "previous": "Önceki", + "next": "İleri", + "previous": "Geri", "submit": "Gönder", "download": "İndir", - "backToHome": "Anasayfaya dön", - "gotIt": "Anladım" + "backToHome": "Ana Sayfaya Dön", + "viewing": "Görüntüleme", + "editing": "Düzenleme", + "gotIt": "Anladım", + "retry": "Tekrar dene", + "uploadFailed": "Yükleme başarısız.", + "copyLinkOriginal": "Orijinal bağlantıyı kopyala" }, "label": { "welcome": "Hoş Geldiniz!", "firstName": "Ad", "middleName": "İkinci Ad", "lastName": "Soyad", - "stepX": "{X}. Adım" + "stepX": "Adım {X}" }, "oAuth": { "err": { "failedTitle": "Hesabınıza bağlanılamıyor.", - "failedMsg": "Lütfen tarayıcınızda giriş işlemini tamamladığınızdan emin olun." + "failedMsg": "Lütfen tarayıcınızda oturum açma işlemini tamamladığınızdan emin olun." }, "google": { - "title": "GOOGLE GİRİŞİ", - "instruction1": "Google Kişilerinizi içe aktarmak için, bu uygulamayı web tarayıcınızdan yetkilendirmeniz gerekecek.", - "instruction2": "Simgeye tıklayarak veya metni seçerek bu kodu panoya kopyalayın:", + "title": "GOOGLE İLE GİRİŞ", + "instruction1": "Google Kişilerinizi içe aktarmak için, bu uygulamaya web tarayıcınızı kullanarak yetki vermeniz gerekecek.", + "instruction2": "Simgeye tıklayarak veya metni seçerek bu kodu panonuza kopyalayın:", "instruction3": "Web tarayıcınızda aşağıdaki bağlantıya gidin ve yukarıdaki kodu girin:", - "instruction4": "Kayıt işlemini tamamladığınızda aşağıdaki butona basın:" + "instruction4": "Kaydı tamamladığınızda aşağıdaki düğmeye basın:" } }, "settings": { "title": "Ayarlar", "popupMenuItem": { "settings": "Ayarlar", + "members": "Üyeler", "trash": "Çöp Kutusu", "helpAndSupport": "Yardım ve Destek" }, + "sites": { + "title": "Siteler", + "namespaceTitle": "Alan Adı", + "namespaceDescription": "Alan adınızı ve ana sayfanızı yönetin", + "namespaceHeader": "Alan Adı", + "homepageHeader": "Ana Sayfa", + "updateNamespace": "Alan adını güncelle", + "removeHomepage": "Ana sayfayı kaldır", + "selectHomePage": "Bir sayfa seç", + "clearHomePage": "Bu alan adı için ana sayfayı temizle", + "customUrl": "Özel URL", + "namespace": { + "description": "Bu değişiklik, bu alan adında yayınlanan tüm canlı sayfalara uygulanacak", + "tooltip": "Uygunsuz alan adlarını kaldırma hakkını saklı tutarız", + "updateExistingNamespace": "Mevcut alan adını güncelle", + "upgradeToPro": "Ana sayfa ayarlamak için Pro Plana yükseltin", + "redirectToPayment": "Ödeme sayfasına yönlendiriliyor...", + "onlyWorkspaceOwnerCanSetHomePage": "Yalnızca çalışma alanı sahibi ana sayfa ayarlayabilir", + "pleaseAskOwnerToSetHomePage": "Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin" + }, + "publishedPage": { + "title": "Tüm yayınlanan sayfalar", + "description": "Yayınlanan sayfalarınızı yönetin", + "page": "Sayfa", + "pathName": "Yol adı", + "date": "Yayınlanma tarihi", + "emptyHinText": "Bu çalışma alanında yayınlanmış sayfanız yok", + "noPublishedPages": "Yayınlanmış sayfa yok", + "settings": "Yayın ayarları", + "clickToOpenPageInApp": "Sayfayı uygulamada aç", + "clickToOpenPageInBrowser": "Sayfayı tarayıcıda aç" + }, + "error": { + "failedToGeneratePaymentLink": "Pro Plan için ödeme bağlantısı oluşturulamadı", + "failedToUpdateNamespace": "Alan adı güncellenemedi", + "proPlanLimitation": "Alan adını güncellemek için Pro Plana yükseltmeniz gerekiyor", + "namespaceAlreadyInUse": "Bu alan adı zaten alınmış, lütfen başka bir tane deneyin", + "invalidNamespace": "Geçersiz alan adı, lütfen başka bir tane deneyin", + "namespaceLengthAtLeast2Characters": "Alan adı en az 2 karakter uzunluğunda olmalıdır", + "onlyWorkspaceOwnerCanUpdateNamespace": "Alan adını yalnızca çalışma alanı sahibi güncelleyebilir", + "onlyWorkspaceOwnerCanRemoveHomepage": "Ana sayfayı yalnızca çalışma alanı sahibi kaldırabilir", + "setHomepageFailed": "Ana sayfa ayarlanamadı", + "namespaceTooLong": "Alan adı çok uzun, lütfen başka bir tane deneyin", + "namespaceTooShort": "Alan adı çok kısa, lütfen başka bir tane deneyin", + "namespaceIsReserved": "Bu alan adı rezerve edilmiş, lütfen başka bir tane deneyin", + "updatePathNameFailed": "Yol adı güncellenemedi", + "removeHomePageFailed": "Ana sayfa kaldırılamadı", + "publishNameContainsInvalidCharacters": "Yol adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin", + "publishNameTooShort": "Yol adı çok kısa, lütfen başka bir tane deneyin", + "publishNameTooLong": "Yol adı çok uzun, lütfen başka bir tane deneyin", + "publishNameAlreadyInUse": "Bu yol adı zaten kullanımda, lütfen başka bir tane deneyin", + "namespaceContainsInvalidCharacters": "Alan adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin", + "publishPermissionDenied": "Yayın ayarlarını yalnızca çalışma alanı sahibi veya sayfa yayıncısı yönetebilir", + "publishNameCannotBeEmpty": "Yol adı boş olamaz, lütfen başka bir tane deneyin" + }, + "success": { + "namespaceUpdated": "Alan adı başarıyla güncellendi", + "setHomepageSuccess": "Ana sayfa başarıyla ayarlandı", + "updatePathNameSuccess": "Yol adı başarıyla güncellendi", + "removeHomePageSuccess": "Ana sayfa başarıyla kaldırıldı" + } + }, "accountPage": { "menuLabel": "Hesabım", "title": "Hesabım", @@ -424,28 +569,28 @@ }, "login": { "title": "Hesap girişi", - "loginLabel": "Giriş Yap", - "logoutLabel": "Çıkış Yap" + "loginLabel": "Giriş yap", + "logoutLabel": "Çıkış yap" } }, "workspacePage": { "menuLabel": "Çalışma Alanı", "title": "Çalışma Alanı", - "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih/saat formatını ve dilini özelleştirin.", + "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih/saat biçimini ve dilini özelleştirin.", "workspaceName": { "title": "Çalışma alanı adı" }, "workspaceIcon": { "title": "Çalışma alanı simgesi", - "description": "Çalışma alanınız için bir resim yükleyin veya bir emoji kullanın. Simge, kenar çubuğunuzda ve bildirimlerinizde görünecektir." + "description": "Çalışma alanınız için bir resim yükleyin veya emoji kullanın. Simge, kenar çubuğunuzda ve bildirimlerinizde görünecektir." }, "appearance": { "title": "Görünüm", - "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarihini, saatini ve dilini özelleştirin.", + "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.", "options": { "system": "Otomatik", - "light": "Açık", - "dark": "Koyu" + "light": "Aydınlık", + "dark": "Karanlık" } }, "resetCursorColor": { @@ -456,10 +601,13 @@ "title": "Belge seçim rengini sıfırla", "description": "Seçim rengini sıfırlamak istediğinizden emin misiniz?" }, + "resetWidth": { + "resetSuccess": "Belge genişliği başarıyla sıfırlandı" + }, "theme": { "title": "Tema", "description": "Önceden ayarlanmış bir tema seçin veya kendi özel temanızı yükleyin.", - "uploadCustomThemeTooltip": "Özel bir tema yükle" + "uploadCustomThemeTooltip": "Özel tema yükle" }, "workspaceFont": { "title": "Çalışma alanı yazı tipi", @@ -479,14 +627,14 @@ }, "dateTime": { "title": "Tarih ve saat", - "example": "{} tarihinde {} ({})", - "24HourTime": "24 saatlik zaman", + "example": "{} {} ({})", + "24HourTime": "24 saat biçimi", "dateFormat": { - "label": "Tarih formatı", + "label": "Tarih biçimi", "local": "Yerel", "us": "ABD", "iso": "ISO", - "friendly": "Dostça", + "friendly": "Kullanıcı dostu", "dmy": "G/A/Y" } }, @@ -495,62 +643,63 @@ }, "deleteWorkspacePrompt": { "title": "Çalışma alanını sil", - "content": "Bu çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfalar yayınlanmamış olacaktır." + "content": "Bu çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır." }, "leaveWorkspacePrompt": { - "title": "Çalışma alanından çık", - "content": "Bu çalışma alanından çıkmak istediğinizden emin misiniz? İçindeki tüm sayfalara ve verilere erişiminizi kaybedeceksiniz.", - "success": "Çalışma alanından başarıyla ayrıldınız." + "title": "Çalışma alanından ayrıl", + "content": "Bu çalışma alanından ayrılmak istediğinizden emin misiniz? İçindeki tüm sayfalara ve verilere erişiminizi kaybedeceksiniz.", + "success": "Çalışma alanından başarıyla ayrıldınız.", + "fail": "Çalışma alanından ayrılınamadı." }, "manageWorkspace": { "title": "Çalışma alanını yönet", - "leaveWorkspace": "Çalışma alanından çık", + "leaveWorkspace": "Çalışma alanından ayrıl", "deleteWorkspace": "Çalışma alanını sil" } }, "manageDataPage": { - "menuLabel": "Verileri Yönet", - "title": "Verileri Yönet", - "description": "Veri yerel depolamayı yönetin veya mevcut verilerinizi @:appName'a aktarın.", + "menuLabel": "Verileri yönet", + "title": "Verileri yönet", + "description": "Yerel depolama verilerini yönetin veya mevcut verilerinizi @:appName'e aktarın.", "dataStorage": { "title": "Dosya depolama konumu", "tooltip": "Dosyalarınızın depolandığı konum", "actions": { "change": "Yolu değiştir", "open": "Klasörü aç", - "openTooltip": "Geçerli veri klasörü konumunu aç", + "openTooltip": "Mevcut veri klasörü konumunu aç", "copy": "Yolu kopyala", "copiedHint": "Yol kopyalandı!", "resetTooltip": "Varsayılan konuma sıfırla" }, "resetDialog": { "title": "Emin misiniz?", - "description": "Yolu varsayılan veri konumuna sıfırlamak verilerinizi silmez. Geçerli verilerinizi yeniden almak istiyorsanız, önce geçerli konumunuzun yolunu kopyalamanız gerekir." + "description": "Yolu varsayılan veri konumuna sıfırlamak verilerinizi silmeyecektir. Mevcut verilerinizi yeniden içe aktarmak istiyorsanız, önce mevcut konumunuzun yolunu kopyalamalısınız." } }, "importData": { - "title": "Veri al", - "tooltip": "@:appName yedeklerinden/veri klasörlerinden veri al", - "description": "Harici bir @:appName veri klasöründen veri kopyalayın", + "title": "Veri içe aktar", + "tooltip": "@:appName yedeklerinden/veri klasörlerinden veri içe aktar", + "description": "Harici bir @:appName veri klasöründen veri kopyala", "action": "Dosyaya göz at" }, "encryption": { "title": "Şifreleme", "tooltip": "Verilerinizin nasıl depolandığını ve şifrelendiğini yönetin", "descriptionNoEncryption": "Şifrelemeyi açmak tüm verileri şifreleyecektir. Bu işlem geri alınamaz.", - "descriptionEncrypted": "Verileriniz şifrelendi.", + "descriptionEncrypted": "Verileriniz şifrelenmiş.", "action": "Verileri şifrele", "dialog": { - "title": "Tüm verilerinizi şifrelemek ister misiniz?", - "description": "Tüm verilerinizi şifrelemek verilerinizi güvende ve emniyette tutacak. Bu işlem geri alınamaz. Devam etmek istediğinizden emin misiniz?" + "title": "Tüm verileriniz şifrelensin mi?", + "description": "Tüm verilerinizi şifrelemek, verilerinizi güvenli ve emniyetli tutacaktır. Bu işlem GERİ ALINAMAZ. Devam etmek istediğinizden emin misiniz?" } }, "cache": { "title": "Önbelleği temizle", - "description": "Görüntülerin yüklenmemesinde veya yazı tiplerinin düzgün görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleğinizi temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmaz.", + "description": "Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.", "dialog": { "title": "Önbelleği temizle", - "description": "Görüntülerin yüklenmemesinde veya yazı tiplerinin düzgün görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleğinizi temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmaz.", + "description": "Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.", "successHint": "Önbellek temizlendi!" } }, @@ -566,37 +715,37 @@ "editBindingHint": "Yeni bağlama girin", "searchHint": "Ara", "actions": { - "resetDefault": "Varsayılanı sıfırla" + "resetDefault": "Varsayılana sıfırla" }, "errorPage": { "message": "Kısayollar yüklenemedi: {}", - "howToFix": "Lütfen tekrar deneyin, sorun devam ederse lütfen GitHub'da bize ulaşın." + "howToFix": "Lütfen tekrar deneyin, sorun devam ederse GitHub üzerinden bize ulaşın." }, "resetDialog": { "title": "Kısayolları sıfırla", - "description": "Bu, tüm tuş bağlamalarınızı varsayılana sıfırlayacaktır, bunu daha sonra geri alamazsınız, devam etmek istediğinizden emin misiniz?", + "description": "Bu işlem tüm tuş bağlamalarınızı varsayılana sıfırlayacak, daha sonra geri alamazsınız, devam etmek istediğinizden emin misiniz?", "buttonLabel": "Sıfırla" }, "conflictDialog": { "title": "{} şu anda kullanımda", - "descriptionPrefix": "Bu tuş bağlaması şu anda", - "descriptionSuffix": "tarafından kullanılıyor. Bu tuş bağlamasını değiştirirseniz, {}'dan kaldırılacaktır.", - "confirmLabel": "Devam Et" + "descriptionPrefix": "Bu tuş bağlaması şu anda ", + "descriptionSuffix": " tarafından kullanılıyor. Bu tuş bağlamasını değiştirirseniz, {} üzerinden kaldırılacak.", + "confirmLabel": "Devam et" }, "editTooltip": "Tuş bağlamasını düzenlemeye başlamak için basın", "keybindings": { - "toggleToDoList": "Yapılacaklar listesini değiştir", + "toggleToDoList": "Yapılacaklar listesini aç/kapat", "insertNewParagraphInCodeblock": "Yeni paragraf ekle", "pasteInCodeblock": "Kod bloğuna yapıştır", "selectAllCodeblock": "Tümünü seç", "indentLineCodeblock": "Satır başına iki boşluk ekle", - "outdentLineCodeblock": "Satır başındaki iki boşluğu sil", - "twoSpacesCursorCodeblock": "İmlece iki boşluk ekle", + "outdentLineCodeblock": "Satır başından iki boşluk sil", + "twoSpacesCursorCodeblock": "İmleç konumuna iki boşluk ekle", "copy": "Seçimi kopyala", - "paste": "İçeriğe yapıştır", + "paste": "İçeriği yapıştır", "cut": "Seçimi kes", "alignLeft": "Metni sola hizala", - "alignCenter": "Metni ortaya hizala", + "alignCenter": "Metni ortala", "alignRight": "Metni sağa hizala", "undo": "Geri al", "redo": "Yinele", @@ -604,9 +753,9 @@ "backspace": "Sil", "deleteLeftWord": "Sol kelimeyi sil", "deleteLeftSentence": "Sol cümleyi sil", - "delete": "Sağ karakteri sil", - "deleteMacOS": "Sol karakteri sil", - "deleteRightWord": "Sağ kelimeyi sil", + "delete": "Sağdaki karakteri sil", + "deleteMacOS": "Soldaki karakteri sil", + "deleteRightWord": "Sağdaki kelimeyi sil", "moveCursorLeft": "İmleci sola taşı", "moveCursorBeginning": "İmleci başa taşı", "moveCursorLeftWord": "İmleci bir kelime sola taşı", @@ -616,54 +765,54 @@ "moveCursorRight": "İmleci sağa taşı", "moveCursorEnd": "İmleci sona taşı", "moveCursorRightWord": "İmleci bir kelime sağa taşı", - "moveCursorRightSelect": "Seç ve imleci bir sağa taşı", + "moveCursorRightSelect": "Seç ve imleci sağa taşı", "moveCursorEndSelect": "Seç ve imleci sona taşı", "moveCursorRightWordSelect": "Seç ve imleci bir kelime sağa taşı", "moveCursorUp": "İmleci yukarı taşı", - "moveCursorTopSelect": "Seç ve imleci üste taşı", - "moveCursorTop": "İmleci üste taşı", + "moveCursorTopSelect": "Seç ve imleci en üste taşı", + "moveCursorTop": "İmleci en üste taşı", "moveCursorUpSelect": "Seç ve imleci yukarı taşı", - "moveCursorBottomSelect": "Seç ve imleci alta taşı", - "moveCursorBottom": "İmleci alta taşı", + "moveCursorBottomSelect": "Seç ve imleci en alta taşı", + "moveCursorBottom": "İmleci en alta taşı", "moveCursorDown": "İmleci aşağı taşı", "moveCursorDownSelect": "Seç ve imleci aşağı taşı", - "home": "Üste kaydır", - "end": "Alta kaydır", - "toggleBold": "Kalınlığı değiştir", - "toggleItalic": "İtalikliği değiştir", - "toggleUnderline": "Altı çiziliyi değiştir", - "toggleStrikethrough": "Üstü çiziliyi değiştir", - "toggleCode": "Satır içi kodu değiştir", - "toggleHighlight": "Vurgulamayı değiştir", + "home": "En üste kaydır", + "end": "En alta kaydır", + "toggleBold": "Kalın yazıyı aç/kapat", + "toggleItalic": "İtalik yazıyı aç/kapat", + "toggleUnderline": "Altı çizili yazıyı aç/kapat", + "toggleStrikethrough": "Üstü çizili yazıyı aç/kapat", + "toggleCode": "Satır içi kodu aç/kapat", + "toggleHighlight": "Vurgulamayı aç/kapat", "showLinkMenu": "Bağlantı menüsünü göster", "openInlineLink": "Satır içi bağlantıyı aç", "openLinks": "Seçili tüm bağlantıları aç", - "indent": "Girinti", - "outdent": "Girintiyi kaldır", - "exit": "Düzenlemeyi çık", + "indent": "Girinti ekle", + "outdent": "Girintiyi azalt", + "exit": "Düzenlemeden çık", "pageUp": "Bir sayfa yukarı kaydır", "pageDown": "Bir sayfa aşağı kaydır", "selectAll": "Tümünü seç", - "pasteWithoutFormatting": "İçeriği biçimlendirmeden yapıştır", + "pasteWithoutFormatting": "İçeriği biçimlendirme olmadan yapıştır", "showEmojiPicker": "Emoji seçiciyi göster", "enterInTableCell": "Tabloda satır sonu ekle", - "leftInTableCell": "Tabloda bir hücre sola taşı", - "rightInTableCell": "Tabloda bir hücre sağa taşı", - "upInTableCell": "Tabloda bir hücre yukarı taşı", - "downInTableCell": "Tabloda bir hücre aşağı", - "tabInTableCell": "Tabloda bir sonraki kullanılabilir hücreye git", + "leftInTableCell": "Tabloda bir hücre sola git", + "rightInTableCell": "Tabloda bir hücre sağa git", + "upInTableCell": "Tabloda bir hücre yukarı git", + "downInTableCell": "Tabloda bir hücre aşağı git", + "tabInTableCell": "Tabloda sonraki kullanılabilir hücreye git", "shiftTabInTableCell": "Tabloda önceki kullanılabilir hücreye git", - "backSpaceInTableCell": "Hücrenin başına dur" + "backSpaceInTableCell": "Hücrenin başında dur" }, "commands": { "codeBlockNewParagraph": "Kod bloğunun yanına yeni bir paragraf ekle", - "codeBlockIndentLines": "Kod bloğunda satır başında iki boşluk ekleyin", - "codeBlockOutdentLines": "Kod bloğundaki satır başındaki iki boşluğu silin", - "codeBlockAddTwoSpaces": "Kod bloğunda imleç konumuna iki boşluk ekleyin", - "codeBlockSelectAll": "Kod bloğunun içindeki tüm içeriği seçin", - "codeBlockPasteText": "Kod bloğuna metin yapıştırın", + "codeBlockIndentLines": "Kod bloğunda satır başına iki boşluk ekle", + "codeBlockOutdentLines": "Kod bloğunda satır başından iki boşluk sil", + "codeBlockAddTwoSpaces": "Kod bloğunda imleç konumuna iki boşluk ekle", + "codeBlockSelectAll": "Kod bloğu içindeki tüm içeriği seç", + "codeBlockPasteText": "Kod bloğuna metin yapıştır", "textAlignLeft": "Metni sola hizala", - "textAlignCenter": "Metni ortaya hizala", + "textAlignCenter": "Metni ortala", "textAlignRight": "Metni sağa hizala" }, "couldNotLoadErrorMsg": "Kısayollar yüklenemedi, tekrar deneyin", @@ -673,28 +822,35 @@ "title": "Yapay Zeka Ayarları", "menuLabel": "Yapay Zeka Ayarları", "keys": { - "enableAISearchTitle": "Yapay Zeka Araması", - "aiSettingsDescription": "@:appName'da kullanılan Yapay Zeka modellerini seçin veya yapılandırın. En iyi performans için varsayılan model seçeneklerini kullanmanızı öneririz", - "loginToEnableAIFeature": "Yapay zeka özellikleri yalnızca @:appName Cloud ile giriş yaptıktan sonra etkinleştirilir. Bir @:appName hesabınız yoksa, kaydolmak için 'Hesabım'a gidin", + "enableAISearchTitle": "Yapay Zeka Arama", + "aiSettingsDescription": "AppFlowy Yapay Zeka'yı güçlendirmek için tercih ettiğiniz modeli seçin. Şu anda GPT 4-o, Claude 3,5, Llama 3.1 ve Mistral 7B içerir", + "loginToEnableAIFeature": "Yapay Zeka özellikleri yalnızca @:appName Cloud ile giriş yaptıktan sonra etkinleştirilir. Bir @:appName hesabınız yoksa, kaydolmak için 'Hesabım'a gidin", "llmModel": "Dil Modeli", "llmModelType": "Dil Modeli Türü", - "downloadLLMPrompt": "{}'ı indir", - "downloadLLMPromptDetail": "{} yerel modelini indirmek {}'a kadar depolama alanı kaplayacaktır. Devam etmek istiyor musunuz?", - "downloadAIModelButton": "Yapay Zeka modelini indir", + "downloadLLMPrompt": "{} İndir", + "downloadAppFlowyOfflineAI": "Yapay Zeka çevrimdışı paketini indirmek, Yapay Zeka'nın cihazınızda çalışmasını sağlayacak. Devam etmek istiyor musunuz?", + "downloadLLMPromptDetail": "{} yerel modelini indirmek {} depolama alanı kullanacak. Devam etmek istiyor musunuz?", + "downloadBigFilePrompt": "İndirmenin tamamlanması yaklaşık 10 dakika sürebilir", + "downloadAIModelButton": "İndir", "downloadingModel": "İndiriliyor", "localAILoaded": "Yerel Yapay Zeka Modeli başarıyla eklendi ve kullanıma hazır", - "localAIStart": "Yerel Yapay Zeka Sohbeti başlıyor...", - "localAILoading": "Yerel Yapay Zeka Sohbeti Modeli yükleniyor...", + "localAIStart": "Yerel Yapay Zeka Sohbeti başlatılıyor...", + "localAILoading": "Yerel Yapay Zeka Sohbet Modeli yükleniyor...", "localAIStopped": "Yerel Yapay Zeka durduruldu", "failToLoadLocalAI": "Yerel Yapay Zeka başlatılamadı", "restartLocalAI": "Yerel Yapay Zeka'yı Yeniden Başlat", + "disableLocalAITitle": "Yerel Yapay Zeka'yı devre dışı bırak", + "disableLocalAIDescription": "Yerel Yapay Zeka'yı devre dışı bırakmak istiyor musunuz?", "localAIToggleTitle": "Yerel Yapay Zeka'yı etkinleştirmek veya devre dışı bırakmak için değiştirin", - "offlineAIDownload2": "indir", - "activeOfflineAI": "Aktif", + "offlineAIInstruction1": "Çevrimdışı Yapay Zeka'yı etkinleştirmek için", + "offlineAIInstruction2": "talimatları", + "offlineAIInstruction3": "takip edin.", + "offlineAIDownload1": "AppFlowy Yapay Zeka'yı henüz indirmediyseniz, lütfen", + "offlineAIDownload2": "indirin", + "offlineAIDownload3": "önce", + "activeOfflineAI": "Etkin", "downloadOfflineAI": "İndir", - "openModelDirectory": "Klasörü aç", - "disableLocalAIDialog": "Yerel Yapay Zeka'yı devre dışı bırakmak istiyor musunuz?", - "fetchLocalModel": "Yerel model yapılandırmasını getir" + "openModelDirectory": "Klasörü aç" } }, "planPage": { @@ -711,11 +867,11 @@ "aiResponseUsage": "{} / {}", "unlimitedAILabel": "Sınırsız yanıt", "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "AI Cihaz Üzerinde", + "aiMaxBadge": "Yapay Zeka Max", + "aiOnDeviceBadge": "Mac için Cihaz Üzerinde Yapay Zeka", "memberProToggle": "Daha fazla üye ve sınırsız Yapay Zeka", - "aiMaxToggle": "Sınırsız Yapay Zeka yanıtı", - "aiOnDeviceToggle": "Üstün gizlilik için cihaz üzerinde Yapay Zeka", + "aiMaxToggle": "Sınırsız Yapay Zeka ve gelişmiş modellere erişim", + "aiOnDeviceToggle": "Maksimum gizlilik için yerel Yapay Zeka", "aiCredit": { "title": "@:appName Yapay Zeka Kredisi Ekle", "price": "{}", @@ -726,13 +882,13 @@ "infoItemTwo": "Çalışma alanı başına 1.000 yanıt" }, "currentPlan": { - "bannerLabel": "Geçerli plan", + "bannerLabel": "Mevcut plan", "freeTitle": "Ücretsiz", "proTitle": "Pro", "teamTitle": "Takım", - "freeInfo": "Bireyler veya en fazla 3 üyeden oluşan küçük ekipler için mükemmel.", - "proInfo": "En fazla 10 üyeden oluşan küçük ve orta ölçekli ekipler için mükemmel.", - "teamInfo": "Üretken ve iyi organize olmuş tüm ekipler için mükemmel.", + "freeInfo": "2 üyeye kadar bireyler için her şeyi düzenlemek için mükemmel", + "proInfo": "10 üyeye kadar küçük ve orta ölçekli takımlar için mükemmel.", + "teamInfo": "Tüm üretken ve iyi organize edilmiş takımlar için mükemmel.", "upgrade": "Planı değiştir", "canceledInfo": "Planınız iptal edildi, {} tarihinde Ücretsiz plana düşürüleceksiniz." }, @@ -741,24 +897,23 @@ "addLabel": "Ekle", "activeLabel": "Eklendi", "aiMax": { - "title": "AI Max", - "description": "Sınırsız Yapay Zeka'nın kilidini aç", + "title": "Yapay Zeka Max", + "description": "Gelişmiş Yapay Zeka modelleri tarafından desteklenen sınırsız Yapay Zeka yanıtları ve ayda 50 Yapay Zeka görüntüsü", "price": "{}", - "priceInfo": "kullanıcı başına aylık", - "billingInfo": "yıllık faturalandırılır veya {} aylık faturalandırılır" + "priceInfo": "Kullanıcı başına aylık, yıllık faturalandırma" }, "aiOnDevice": { - "title": "AI Cihaz Üzerinde", - "description": "Cihazınızda çevrimdışı Yapay Zeka", + "title": "Mac için Cihaz Üzerinde Yapay Zeka", + "description": "Mistral 7B, LLAMA 3 ve daha fazla yerel modeli makinenizde çalıştırın", "price": "{}", - "priceInfo": "kullanıcı başına aylık", - "billingInfo": "yıllık faturalandırılır veya {} aylık faturalandırılır" + "priceInfo": "Kullanıcı başına aylık, yıllık faturalandırma", + "recommend": "M1 veya daha yenisi önerilir" } }, "deal": { "bannerLabel": "Yeni yıl fırsatı!", - "title": "Ekibinizi büyütün!", - "info": "Yükseltin ve Pro ve Takım planlarında %10 indirim kazanın! @:appName Yapay Zeka dahil güçlü yeni özelliklerle çalışma alanı üretkenliğinizi artırın.", + "title": "Takımınızı büyütün!", + "info": "Yükseltin ve Pro ve Takım planlarında %10 indirim kazanın! @:appName Yapay Zeka dahil güçlü yeni özelliklerle çalışma alanı verimliliğinizi artırın.", "viewPlans": "Planları görüntüle" } } @@ -775,7 +930,7 @@ "periodButtonLabel": "Dönemi düzenle" }, "paymentDetails": { - "title": "Ödeme ayrıntıları", + "title": "Ödeme detayları", "methodLabel": "Ödeme yöntemi", "methodButtonLabel": "Yöntemi düzenle" }, @@ -785,122 +940,125 @@ "removeLabel": "Kaldır", "renewLabel": "Yenile", "aiMax": { - "label": "AI Max", - "description": "Sınırsız Yapay Zeka'nın ve gelişmiş modellerin kilidini aç", - "activeDescription": "Sonraki fatura {} tarihinde ödenecek", - "canceledDescription": "AI Max {} tarihine kadar kullanılabilecek" + "label": "Yapay Zeka Max", + "description": "Sınırsız Yapay Zeka ve gelişmiş modellerin kilidini açın", + "activeDescription": "Sonraki fatura tarihi: {}", + "canceledDescription": "Yapay Zeka Max {} tarihine kadar kullanılabilir olacak" }, "aiOnDevice": { - "label": "AI Cihaz Üzerinde", - "description": "Cihazınızda çevrimdışı sınırsız Yapay Zeka'nın kilidini aç", - "activeDescription": "Sonraki fatura {} tarihinde ödenecek", - "canceledDescription": "AI Cihaz Üzerinde {} tarihine kadar kullanılabilecek" + "label": "Mac için Cihaz Üzerinde Yapay Zeka", + "description": "Cihazınızda sınırsız Cihaz Üzerinde Yapay Zeka'nın kilidini açın", + "activeDescription": "Sonraki fatura tarihi: {}", + "canceledDescription": "Mac için Cihaz Üzerinde Yapay Zeka {} tarihine kadar kullanılabilir olacak" }, "removeDialog": { - "title": "{}'ı kaldır", - "description": "{plan}'ı kaldırmak istediğinizden emin misiniz? {plan}'ın özelliklerine ve avantajlarına hemen erişiminizi kaybedeceksiniz." + "title": "{} Kaldır", + "description": "{plan} planını kaldırmak istediğinizden emin misiniz? {plan} planının özelliklerine ve avantajlarına erişiminizi hemen kaybedeceksiniz." } }, - "currentPeriodBadge": "GEÇERLİ", + "currentPeriodBadge": "MEVCUT", "changePeriod": "Dönemi değiştir", "planPeriod": "{} dönemi", "monthlyInterval": "Aylık", - "monthlyPriceInfo": "koltuk başına aylık faturalandırılır", + "monthlyPriceInfo": "koltuk başına aylık faturalandırma", "annualInterval": "Yıllık", - "annualPriceInfo": "koltuk başına yıllık faturalandırılır" + "annualPriceInfo": "koltuk başına yıllık faturalandırma" }, "comparePlanDialog": { - "title": "Planları karşılaştır ve seç", + "title": "Plan karşılaştır ve seç", "planFeatures": "Plan\nÖzellikleri", - "current": "Geçerli", + "current": "Mevcut", "actions": { "upgrade": "Yükselt", "downgrade": "Düşür", - "current": "Geçerli" + "current": "Mevcut" }, "freePlan": { "title": "Ücretsiz", - "description": "Bireyler ve küçük grupların her şeyi organize etmesi için", + "description": "2 üyeye kadar bireyler için her şeyi düzenlemek için", "price": "{}", - "priceInfo": "sonsuza kadar ücretsiz" + "priceInfo": "Sonsuza kadar ücretsiz" }, "proPlan": { "title": "Pro", - "description": "Küçük ekiplerin projeleri ve ekip bilgisini yönetmesi için", + "description": "Küçük takımların projeleri ve takım bilgisini yönetmesi için", "price": "{}", - "priceInfo": "kullanıcı başına aylık \nyıllık faturalandırılır\n\n{} aylık faturalandırılır" + "priceInfo": "Kullanıcı başına aylık \nyıllık faturalandırma\n\n{} aylık faturalandırma" }, "planLabels": { "itemOne": "Çalışma Alanları", "itemTwo": "Üyeler", "itemThree": "Depolama", - "itemFour": "Gerçek zamanlı iş birliği", + "itemFour": "Gerçek zamanlı işbirliği", "itemFive": "Mobil uygulama", "itemSix": "Yapay Zeka Yanıtları", - "tooltipSix": "Ömür boyu, yanıt sayısının asla sıfırlanmayacağı anlamına gelir", - "tooltipSeven": "Çalışma alanınız için URL'nin bir bölümünü özelleştirmenize olanak tanır", - "itemSeven": "Özel ad alanı" + "itemFileUpload": "Dosya yüklemeleri", + "customNamespace": "Özel alan adı", + "tooltipSix": "Ömür boyu demek, yanıt sayısının asla sıfırlanmayacağı anlamına gelir", + "intelligentSearch": "Akıllı arama", + "tooltipSeven": "Çalışma alanınızın URL'sinin bir kısmını özelleştirmenize olanak tanır", + "customNamespaceTooltip": "Özel yayınlanmış site URL'si" }, "freeLabels": { - "itemOne": "çalışma alanı başına ücretlendirilir", - "itemTwo": "en fazla 3", + "itemOne": "Çalışma alanı başına ücretlendirilir", + "itemTwo": "2'ye kadar", "itemThree": "5 GB", "itemFour": "evet", "itemFive": "evet", "itemSix": "10 ömür boyu", "itemFileUpload": "7 MB'a kadar", - "itemSeven": " " + "intelligentSearch": "Akıllı arama" }, "proLabels": { - "itemOne": "çalışma alanı başına ücretlendirilir", - "itemTwo": "en fazla 10", - "itemThree": "sınırsız", + "itemOne": "Çalışma alanı başına ücretlendirilir", + "itemTwo": "10'a kadar", + "itemThree": "Sınırsız", "itemFour": "evet", "itemFive": "evet", - "itemSix": "sınırsız", + "itemSix": "Sınırsız", "itemFileUpload": "Sınırsız", - "itemSeven": " " + "intelligentSearch": "Akıllı arama" }, "paymentSuccess": { "title": "Artık {} planındasınız!", - "description": "Ödemeniz başarıyla işlendi ve planınız @:appName {} olarak yükseltildi. Plan ayrıntılarınızı Plan sayfasında görüntüleyebilirsiniz" + "description": "Ödemeniz başarıyla işleme alındı ve planınız @:appName {}'e yükseltildi. Plan detaylarınızı Plan sayfasında görüntüleyebilirsiniz" }, "downgradeDialog": { "title": "Planınızı düşürmek istediğinizden emin misiniz?", - "description": "Planınızı düşürmek sizi Ücretsiz plana geri döndürecektir. Üyeler bu çalışma alanına erişimini kaybedebilir ve Ücretsiz planın depolama sınırlarını karşılamak için alan boşaltmanız gerekebilir.", + "description": "Planınızı düşürmek sizi Ücretsiz plana geri döndürecek. Üyeler bu çalışma alanına erişimlerini kaybedebilir ve Ücretsiz planın depolama sınırlarına uymak için alan açmanız gerekebilir.", "downgradeLabel": "Planı düşür" } }, "cancelSurveyDialog": { - "title": "Gittiğinizi görmek üzücü", - "description": "Gittiğinizi görmek üzücü. @:appName'ı geliştirmemize yardımcı olmak için geri bildirimlerinizi duymak isteriz. Lütfen birkaç soruya cevaplamak için bir dakikanızı ayırın.", + "title": "Gitmenize üzüldük", + "description": "Gitmenize üzüldük. @:appName'i geliştirmemize yardımcı olmak için geri bildiriminizi duymak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.", "commonOther": "Diğer", - "otherHint": "Cevabınızı buraya yazın", + "otherHint": "Yanıtınızı buraya yazın", "questionOne": { - "question": "AppFlowy Pro aboneliğinizi iptal etmenize ne sebep oldu?", + "question": "@:appName Pro aboneliğinizi iptal etmenize ne sebep oldu?", "answerOne": "Maliyet çok yüksek", "answerTwo": "Özellikler beklentileri karşılamadı", "answerThree": "Daha iyi bir alternatif buldum", - "answerFour": "Gideri haklı çıkarmak için yeterince kullanmadım", + "answerFour": "Maliyeti haklı çıkaracak kadar kullanmadım", "answerFive": "Hizmet sorunu veya teknik zorluklar" }, "questionTwo": { - "question": "Gelecekte AppFlowy Pro'ya yeniden abone olmayı ne kadar olası görüyorsunuz?", - "answerOne": "Çok olası", - "answerTwo": "Biraz olası", + "question": "Gelecekte @:appName Pro'ya yeniden abone olma olasılığınız nedir?", + "answerOne": "Çok muhtemel", + "answerTwo": "Biraz muhtemel", "answerThree": "Emin değilim", - "answerFour": "Olası değil", - "answerFive": "Hiç olası değil" + "answerFour": "Muhtemel değil", + "answerFive": "Hiç muhtemel değil" }, "questionThree": { "question": "Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?", - "answerOne": "Çok kullanıcılı iş birliği", - "answerTwo": "Daha uzun süreli sürüm geçmişi", - "answerThree": "Sınırsız Yapay Zeka yanıtı", + "answerOne": "Çoklu kullanıcı işbirliği", + "answerTwo": "Daha uzun süreli versiyon geçmişi", + "answerThree": "Sınırsız Yapay Zeka yanıtları", "answerFour": "Yerel Yapay Zeka modellerine erişim" }, "questionFour": { - "question": "AppFlowy ile genel deneyiminizi nasıl tanımlarsınız?", + "question": "@:appName ile genel deneyiminizi nasıl tanımlarsınız?", "answerOne": "Harika", "answerTwo": "İyi", "answerThree": "Ortalama", @@ -909,7 +1067,8 @@ } }, "common": { - "uploadingFile": "Dosya yükleniyor. Lütfen uygulamadan ayrılmayın", + "uploadingFile": "Dosya yükleniyor. Lütfen uygulamadan çıkmayın", + "uploadNotionSuccess": "Notion zip dosyanız başarıyla yüklendi. İçe aktarma tamamlandığında bir onay e-postası alacaksınız", "reset": "Sıfırla" }, "menu": { @@ -921,46 +1080,51 @@ "open": "Ayarları Aç", "logout": "Çıkış Yap", "logoutPrompt": "Çıkış yapmak istediğinizden emin misiniz?", - "selfEncryptionLogoutPrompt": "Çıkış yapmak istediğinizden emin misiniz? Lütfen şifreleme anahtarını kopyaladığınızdan emin olun", + "selfEncryptionLogoutPrompt": "Çıkış yapmak istediğinizden emin misiniz? Lütfen şifreleme anahtarınızı kopyaladığınızdan emin olun", "syncSetting": "Senkronizasyon Ayarı", "cloudSettings": "Bulut Ayarları", "enableSync": "Senkronizasyonu etkinleştir", + "enableSyncLog": "Senkronizasyon günlüğünü etkinleştir", + "enableSyncLogWarning": "Senkronizasyon sorunlarını teşhis etmeye yardımcı olduğunuz için teşekkür ederiz. Bu, belge düzenlemelerinizi yerel bir dosyaya kaydedecek. Lütfen etkinleştirdikten sonra uygulamayı kapatıp yeniden açın", "enableEncrypt": "Verileri şifrele", "cloudURL": "Temel URL", + "webURL": "Web URL'si", "invalidCloudURLScheme": "Geçersiz Şema", "cloudServerType": "Bulut sunucusu", - "cloudServerTypeTip": "Lütfen bulut sunucusunu değiştirdikten sonra geçerli hesabınızdan çıkış yapabileceğinizi unutmayın", + "cloudServerTypeTip": "Lütfen bulut sunucusunu değiştirdikten sonra mevcut hesabınızdan çıkış yapabileceğinizi unutmayın", "cloudLocal": "Yerel", - "cloudAppFlowy": "@:appName Cloud Beta", - "cloudAppFlowySelfHost": "@:appName Cloud Kendi Sunucunuzda", - "appFlowyCloudUrlCanNotBeEmpty": "Bulut URL'si boş bırakılamaz", - "clickToCopy": "Kopyalamak için tıklayın", - "selfHostStart": "Bir sunucunuz yoksa, kendi sunucunuzu nasıl kuracağınız konusunda rehberlik için", - "selfHostContent": "belgeye", - "selfHostEnd": "başvurabilirsiniz", + "cloudAppFlowy": "@:appName Cloud", + "cloudAppFlowySelfHost": "@:appName Cloud Kendi Kendine Barındırma", + "appFlowyCloudUrlCanNotBeEmpty": "Bulut URL'si boş olamaz", + "clickToCopy": "Panoya kopyala", + "selfHostStart": "Bir sunucunuz yoksa, lütfen", + "selfHostContent": "belgesine", + "selfHostEnd": "bakarak kendi sunucunuzu nasıl barındıracağınızı öğrenin", "pleaseInputValidURL": "Lütfen geçerli bir URL girin", + "changeUrl": "Kendi kendine barındırılan URL'yi {} olarak değiştir", "cloudURLHint": "Sunucunuzun temel URL'sini girin", + "webURLHint": "Web sunucunuzun temel URL'sini girin", "cloudWSURL": "Websocket URL'si", "cloudWSURLHint": "Sunucunuzun websocket adresini girin", "restartApp": "Yeniden Başlat", - "restartAppTip": "Değişikliklerin etkili olması için uygulamayı yeniden başlatın. Lütfen bunun geçerli hesabınızdan çıkış yapabileceğini unutmayın.", - "changeServerTip": "Sunucuyu değiştirdikten sonra, değişikliklerin etkili olması için yeniden başlat düğmesine tıklamanız gerekir", - "enableEncryptPrompt": "Verilerinizi korumak için şifrelemeyi etkinleştirin. Bu işlem geri alınamaz, bu yüzden şifreleme anahtarınızı güvenli bir yerde saklamanız çok önemlidir. Kopyalamak için tıklayın", - "inputEncryptPrompt": "Lütfen şifreleme anahtarınızı girin", + "restartAppTip": "Değişikliklerin etkili olması için uygulamayı yeniden başlatın. Bu işlemin mevcut hesabınızdan çıkış yapabileceğini unutmayın.", + "changeServerTip": "Sunucuyu değiştirdikten sonra, değişikliklerin etkili olması için yeniden başlat düğmesine tıklamalısınız", + "enableEncryptPrompt": "Verilerinizi bu anahtar ile güvence altına almak için şifrelemeyi etkinleştirin. Güvenli bir şekilde saklayın; etkinleştirildikten sonra kapatılamaz. Kaybedilirse, verileriniz kurtarılamaz hale gelir. Kopyalamak için tıklayın", + "inputEncryptPrompt": "Lütfen şifreleme anahtarlarını girin", "clickToCopySecret": "Anahtarı kopyalamak için tıklayın", "configServerSetting": "Sunucu ayarlarınızı yapılandırın", - "configServerGuide": "`Hızlı Başlangıç`ı seçtikten sonra, kendi sunucunuzu yapılandırmak için `Ayarlar` ve ardından \"Bulut Ayarları\"na gidin.", + "configServerGuide": "'Hızlı Başlangıç'ı seçtikten sonra, 'Ayarlar'a ve ardından \"Bulut Ayarları\"na giderek kendi kendine barındırılan sunucunuzu yapılandırın.", "inputTextFieldHint": "Anahtarınız", "historicalUserList": "Kullanıcı giriş geçmişi", - "historicalUserListTooltip": "Bu liste anonim hesaplarınızı görüntüler. Ayrıntılarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başlayın' düğmesine tıklanarak oluşturulur", + "historicalUserListTooltip": "Bu liste anonim hesaplarınızı gösterir. Detaylarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başla' düğmesine tıklanarak oluşturulur", "openHistoricalUser": "Anonim hesabı açmak için tıklayın", - "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", - "importAppFlowyData": "Harici @:appName Klasöründen Veri Al", - "importingAppFlowyDataTip": "Veri aktarımı devam ediyor. Lütfen uygulamayı kapatmayın", - "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve geçerli AppFlowy veri klasörüne aktarın", - "importSuccess": "@:appName veri klasörü başarıyla alındı", - "importFailed": "@:appName veri klasörü alınamadı", - "importGuide": "Daha fazla bilgi için lütfen referans belgeyi kontrol edin" + "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulut senkronizasyonlu bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmaları meydana gelebilir", + "importAppFlowyData": "Harici @:appName Klasöründen Veri İçe Aktar", + "importingAppFlowyDataTip": "Veri içe aktarma devam ediyor. Lütfen uygulamayı kapatmayın", + "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve mevcut AppFlowy veri klasörüne aktarın", + "importSuccess": "@:appName veri klasörü başarıyla içe aktarıldı", + "importFailed": "@:appName veri klasörünün içe aktarılması başarısız oldu", + "importGuide": "Daha fazla detay için lütfen referans belgeyi kontrol edin" }, "notifications": { "enableNotifications": { @@ -968,7 +1132,7 @@ "hint": "Yerel bildirimlerin görünmesini durdurmak için kapatın." }, "showNotificationsIcon": { - "label": "Bildirimler simgesini göster", + "label": "Bildirim simgesini göster", "hint": "Kenar çubuğundaki bildirim simgesini gizlemek için kapatın." }, "archiveNotifications": { @@ -981,17 +1145,30 @@ }, "action": { "markAsRead": "Okundu olarak işaretle", - "archive": "Arşiv" + "multipleChoice": "Daha fazla seç", + "archive": "Arşivle" }, "settings": { "settings": "Ayarlar", "markAllAsRead": "Tümünü okundu olarak işaretle", "archiveAll": "Tümünü arşivle" }, + "emptyInbox": { + "title": "Gelen Kutusu Boş!", + "description": "Burada bildirim almak için hatırlatıcılar ayarlayın." + }, + "emptyUnread": { + "title": "Okunmamış bildirim yok", + "description": "Hepsini okudunuz!" + }, + "emptyArchived": { + "title": "Arşivlenmiş öğe yok", + "description": "Arşivlenen bildirimler burada görünecek." + }, "tabs": { - "inbox": "Gelen kutusu", + "inbox": "Gelen Kutusu", "unread": "Okunmamış", - "archived": "Arşivlendi" + "archived": "Arşivlenmiş" }, "refreshSuccess": "Bildirimler başarıyla yenilendi", "titles": { @@ -1008,21 +1185,24 @@ }, "themeMode": { "label": "Tema Modu", - "light": "Açık Mod", - "dark": "Koyu Mod", - "system": "Sisteme Uyarla" + "light": "Aydınlık Mod", + "dark": "Karanlık Mod", + "system": "Sisteme Uyum Sağla" }, - "fontScaleFactor": "Yazı Tipi Ölçeklendirme Faktörü", + "fontScaleFactor": "Yazı Tipi Ölçek Faktörü", + "displaySize": "Görüntüleme Boyutu", "documentSettings": { "cursorColor": "Belge imleç rengi", "selectionColor": "Belge seçim rengi", - "pickColor": "Bir renk seçin", + "width": "Belge genişliği", + "changeWidth": "Değiştir", + "pickColor": "Bir renk seç", "colorShade": "Renk tonu", "opacity": "Opaklık", - "hexEmptyError": "Hex rengi boş bırakılamaz", - "hexLengthError": "Hex değeri 6 haneli olmalıdır", - "hexInvalidError": "Geçersiz hex değeri", - "opacityEmptyError": "Opaklık boş bırakılamaz", + "hexEmptyError": "Hex renk boş olamaz", + "hexLengthError": "Hex renk 6 haneli olmalıdır", + "hexInvalidError": "Geçersiz hex renk", + "opacityEmptyError": "Opaklık boş olamaz", "opacityRangeError": "Opaklık 1 ile 100 arasında olmalıdır", "app": "Uygulama", "flowy": "Flowy", @@ -1030,119 +1210,125 @@ }, "layoutDirection": { "label": "Düzen Yönü", - "hint": "Ekranınızdaki içeriğin akışını soldan sağa veya sağdan sola doğru kontrol edin.", - "ltr": "LTR", - "rtl": "RTL" + "hint": "Ekranınızdaki içeriğin akışını soldan sağa veya sağdan sola olarak kontrol edin.", + "ltr": "Soldan Sağa", + "rtl": "Sağdan Sola" }, "textDirection": { "label": "Varsayılan Metin Yönü", - "hint": "Metnin varsayılan olarak soldan mı yoksa sağdan mı başlaması gerektiğini belirtin.", - "ltr": "LTR", - "rtl": "RTL", + "hint": "Metnin varsayılan olarak soldan mı yoksa sağdan mı başlayacağını belirtin.", + "ltr": "Soldan Sağa", + "rtl": "Sağdan Sola", "auto": "OTOMATİK", - "fallback": "Düzen yönüyle aynı" + "fallback": "Düzen yönü ile aynı" }, "themeUpload": { "button": "Yükle", - "uploadTheme": "Temayı yükle", + "uploadTheme": "Tema yükle", "description": "Aşağıdaki düğmeyi kullanarak kendi @:appName temanızı yükleyin.", - "loading": "Lütfen temanızı doğrularken ve yüklerken bekleyin...", + "loading": "Temanızı doğrulayıp yüklerken lütfen bekleyin...", "uploadSuccess": "Temanız başarıyla yüklendi", - "deletionFailure": "Temayı silinemedi. Manuel olarak silmeyi deneyin.", - "filePickerDialogTitle": ".flowy_plugin dosyası seçin", + "deletionFailure": "Tema silinemedi. Manuel olarak silmeyi deneyin.", + "filePickerDialogTitle": "Bir .flowy_plugin dosyası seçin", "urlUploadFailure": "URL açılamadı: {}" }, "theme": "Tema", "builtInsLabel": "Yerleşik Temalar", "pluginsLabel": "Eklentiler", "dateFormat": { - "label": "Tarih formatı", + "label": "Tarih biçimi", "local": "Yerel", "us": "ABD", "iso": "ISO", - "friendly": "Dostça", + "friendly": "Kullanıcı dostu", "dmy": "G/A/Y" }, "timeFormat": { - "label": "Saat formatı", - "twelveHour": "12 saatlik", - "twentyFourHour": "24 saatlik" + "label": "Saat biçimi", + "twelveHour": "12 saat", + "twentyFourHour": "24 saat" }, "showNamingDialogWhenCreatingPage": "Sayfa oluştururken adlandırma iletişim kutusunu göster", - "enableRTLToolbarItems": "RTL araç çubuğu öğelerini etkinleştir", + "enableRTLToolbarItems": "Sağdan sola araç çubuğu öğelerini etkinleştir", "members": { - "title": "Üye Ayarları", - "inviteMembers": "Üye Davet Et", + "title": "Üye ayarları", + "inviteMembers": "Üye davet et", "inviteHint": "E-posta ile davet et", - "sendInvite": "Davetiye Gönder", - "copyInviteLink": "Davetiye Bağlantısını Kopyala", + "sendInvite": "Davet gönder", + "copyInviteLink": "Davet bağlantısını kopyala", "label": "Üyeler", "user": "Kullanıcı", "role": "Rol", "removeFromWorkspace": "Çalışma Alanından Kaldır", + "removeFromWorkspaceSuccess": "Çalışma alanından başarıyla kaldırıldı", + "removeFromWorkspaceFailed": "Çalışma alanından kaldırma başarısız oldu", "owner": "Sahip", "guest": "Misafir", "member": "Üye", "memberHintText": "Bir üye sayfaları okuyabilir ve düzenleyebilir", "guestHintText": "Bir Misafir okuyabilir, tepki verebilir, yorum yapabilir ve izin verilen belirli sayfaları düzenleyebilir.", - "emailInvalidError": "Geçersiz e-posta, lütfen kontrol edin ve tekrar deneyin", + "emailInvalidError": "Geçersiz e-posta, lütfen kontrol edip tekrar deneyin", "emailSent": "E-posta gönderildi, lütfen gelen kutunuzu kontrol edin", - "members": "üyeler", + "members": "üye", "membersCount": { "zero": "{} üye", "one": "{} üye", "other": "{} üye" }, - "memberLimitExceeded": "Üye sınırı aşıldı, daha fazla üye davet etmek için lütfen ", + "inviteFailedDialogTitle": "Davet gönderilemedi", + "inviteFailedMemberLimit": "Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen yükseltin.", + "inviteFailedMemberLimitMobile": "Çalışma alanınız üye sınırına ulaştı.", + "memberLimitExceeded": "Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen ", "memberLimitExceededUpgrade": "yükseltin", - "memberLimitExceededPro": "Üye sınırı aşıldı, daha fazla üyeye ihtiyacınız varsa lütfen ", - "memberLimitExceededProContact": "support@appflowy.io ile iletişime geçin", + "memberLimitExceededPro": "Üye sınırına ulaşıldı, daha fazla üye gerekiyorsa ", + "memberLimitExceededProContact": "support@appflowy.io", "failedToAddMember": "Üye eklenemedi", "addMemberSuccess": "Üye başarıyla eklendi", "removeMember": "Üyeyi Kaldır", "areYouSureToRemoveMember": "Bu üyeyi kaldırmak istediğinizden emin misiniz?", - "inviteMemberSuccess": "Davetiye başarıyla gönderildi", + "inviteMemberSuccess": "Davet başarıyla gönderildi", "failedToInviteMember": "Üye davet edilemedi", - "workspaceMembersError": "Oops, bir şeyler ters gitti" + "workspaceMembersError": "Hay aksi, bir şeyler yanlış gitti", + "workspaceMembersErrorDescription": "Şu anda üye listesini yükleyemedik. Lütfen daha sonra tekrar deneyin" } }, "files": { "copy": "Kopyala", - "defaultLocation": "Dosyaları ve verileri okuma konumu", + "defaultLocation": "Dosyaları ve veri depolama konumunu oku", "exportData": "Verilerinizi dışa aktarın", - "doubleTapToCopy": "Yolu kopyalamak için iki kez dokunun", + "doubleTapToCopy": "Yolu kopyalamak için çift dokunun", "restoreLocation": "@:appName varsayılan yoluna geri yükle", "customizeLocation": "Başka bir klasör aç", "restartApp": "Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın.", "exportDatabase": "Veritabanını dışa aktar", "selectFiles": "Dışa aktarılması gereken dosyaları seçin", "selectAll": "Tümünü seç", - "deselectAll": "Tüm seçimleri kaldır", - "createNewFolder": "Yeni bir klasör oluştur", - "createNewFolderDesc": "Verilerinizi nerede saklamak istediğinizi bize bildirin", - "defineWhereYourDataIsStored": "Verilerinizin nerede saklandığını tanımlayın", + "deselectAll": "Tüm seçimi kaldır", + "createNewFolder": "Yeni klasör oluştur", + "createNewFolderDesc": "Verilerinizi nerede saklamak istediğinizi bize söyleyin", + "defineWhereYourDataIsStored": "Verilerinizin nerede saklanacağını tanımlayın", "open": "Aç", - "openFolder": "Mevcut bir klasörü aç", - "openFolderDesc": "Mevcut @:appName klasörünüze okuyun ve yazın", + "openFolder": "Mevcut bir klasör aç", + "openFolderDesc": "Mevcut @:appName klasörünüzü okuyun ve yazın", "folderHintText": "klasör adı", - "location": "Yeni bir klasör oluşturuluyor", + "location": "Yeni klasör oluşturma", "locationDesc": "@:appName veri klasörünüz için bir ad seçin", - "browser": "Gözat", + "browser": "Göz at", "create": "Oluştur", "set": "Ayarla", "folderPath": "Klasörünüzü saklamak için yol", - "locationCannotBeEmpty": "Yol boş bırakılamaz", + "locationCannotBeEmpty": "Yol boş olamaz", "pathCopiedSnackbar": "Dosya depolama yolu panoya kopyalandı!", "changeLocationTooltips": "Veri dizinini değiştir", "change": "Değiştir", "openLocationTooltips": "Başka bir veri dizini aç", - "openCurrentDataFolder": "Geçerli veri dizinini aç", - "recoverLocationTooltips": "@:appName'nin varsayılan veri dizinine sıfırla", + "openCurrentDataFolder": "Mevcut veri dizinini aç", + "recoverLocationTooltips": "@:appName'in varsayılan veri dizinine sıfırla", "exportFileSuccess": "Dosya başarıyla dışa aktarıldı!", "exportFileFail": "Dosya dışa aktarılamadı!", - "export": "Dışa Aktar", + "export": "Dışa aktar", "clearCache": "Önbelleği temizle", - "clearCacheDesc": "Görüntülerin yüklenmemesinde veya yazı tiplerinin düzgün görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleğinizi temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmaz.", + "clearCacheDesc": "Resimlerin yüklenmemesi veya yazı tiplerinin doğru görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleği temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmayacaktır.", "areYouSureToClearCache": "Önbelleği temizlemek istediğinizden emin misiniz?", "clearCacheSuccess": "Önbellek başarıyla temizlendi!" }, @@ -1150,26 +1336,25 @@ "name": "Ad", "email": "E-posta", "tooltipSelectIcon": "Simge seç", - "selectAnIcon": "Bir simge seçin", - "pleaseInputYourOpenAIKey": "Lütfen Yapay Zeka anahtarınızı girin", - "clickToLogout": "Geçerli kullanıcıdan çıkış yapmak için tıklayın", - "pleaseInputYourStabilityAIKey": "Lütfen Stability Yapay Zeka anahtarınızı girin" + "selectAnIcon": "Bir simge seç", + "pleaseInputYourOpenAIKey": "lütfen Yapay Zeka anahtarınızı girin", + "clickToLogout": "Mevcut kullanıcının oturumunu kapatmak için tıklayın" }, "mobile": { "personalInfo": "Kişisel Bilgiler", "username": "Kullanıcı Adı", - "usernameEmptyError": "Kullanıcı adı boş bırakılamaz", + "usernameEmptyError": "Kullanıcı adı boş olamaz", "about": "Hakkında", - "pushNotifications": "Anında Bildirimler", + "pushNotifications": "Anlık Bildirimler", "support": "Destek", "joinDiscord": "Discord'da bize katılın", "privacyPolicy": "Gizlilik Politikası", "userAgreement": "Kullanıcı Sözleşmesi", "termsAndConditions": "Şartlar ve Koşullar", "userprofileError": "Kullanıcı profili yüklenemedi", - "userprofileErrorDescription": "Lütfen sorunun devam edip etmediğini kontrol etmek için çıkış yapıp tekrar giriş yapmayı deneyin.", - "selectLayout": "Düzen seçin", - "selectStartingDay": "Başlangıç gününü seçin", + "userprofileErrorDescription": "Lütfen sorunun devam edip etmediğini kontrol etmek için oturumu kapatıp yeniden giriş yapmayı deneyin.", + "selectLayout": "Düzen seç", + "selectStartingDay": "Başlangıç gününü seç", "version": "Sürüm" } }, @@ -1177,18 +1362,18 @@ "deleteView": "Bu görünümü silmek istediğinizden emin misiniz?", "createView": "Yeni", "title": { - "placeholder": "İsimsiz" + "placeholder": "Başlıksız" }, "settings": { "filter": "Filtre", "sort": "Sırala", - "sortBy": "Şuna göre sırala", + "sortBy": "Sıralama ölçütü", "properties": "Özellikler", "reorderPropertiesTooltip": "Özellikleri yeniden sıralamak için sürükleyin", "group": "Grupla", "addFilter": "Filtre Ekle", "deleteFilter": "Filtreyi sil", - "filterBy": "Şuna göre filtrele...", + "filterBy": "Filtreleme ölçütü", "typeAValue": "Bir değer yazın...", "layout": "Düzen", "databaseLayout": "Düzen", @@ -1201,103 +1386,113 @@ "boardSettings": "Pano ayarları", "calendarSettings": "Takvim ayarları", "createView": "Yeni görünüm", - "duplicateView": "Görünümü kopyala", + "duplicateView": "Görünümü çoğalt", "deleteView": "Görünümü sil", "numberOfVisibleFields": "{} gösteriliyor" }, "filter": { - "addFilter": "Filtre ekle" + "empty": "Aktif filtre yok", + "addFilter": "Filtre ekle", + "cannotFindCreatableField": "Filtrelenecek uygun bir alan bulunamadı", + "conditon": "Koşul", + "where": "Koşul" }, "textFilter": { "contains": "İçerir", "doesNotContain": "İçermez", - "endsWith": "Şununla biter", - "startWith": "Şununla başlar", - "is": "Şudur", - "isNot": "Şu değildir", - "isEmpty": "Boş", - "isNotEmpty": "Boş değil", + "endsWith": "İle biter", + "startWith": "İle başlar", + "is": "Eşittir", + "isNot": "Eşit değildir", + "isEmpty": "Boştur", + "isNotEmpty": "Boş değildir", "choicechipPrefix": { "isNot": "Değil", - "startWith": "Şununla başlar", - "endWith": "Şununla biter", - "isEmpty": "boş", - "isNotEmpty": "boş değil" + "startWith": "İle başlar", + "endWith": "İle biter", + "isEmpty": "boştur", + "isNotEmpty": "boş değildir" } }, "checkboxFilter": { "isChecked": "İşaretli", "isUnchecked": "İşaretsiz", "choicechipPrefix": { - "is": "işaretli" + "is": "eşittir" } }, "checklistFilter": { - "isComplete": "tamamlandı", - "isIncomplted": "tamamlanmadı" + "isComplete": "Tamamlandı", + "isIncomplted": "Tamamlanmadı" }, "selectOptionFilter": { - "is": "Şudur", - "isNot": "Şu değildir", + "is": "Eşittir", + "isNot": "Eşit değildir", "contains": "İçerir", "doesNotContain": "İçermez", - "isEmpty": "Boş", - "isNotEmpty": "Boş değil" + "isEmpty": "Boştur", + "isNotEmpty": "Boş değildir" }, "dateFilter": { - "is": "Şudur", - "before": "Şundan önce", - "after": "Şundan sonra", - "onOrBefore": "Şunda veya önce", - "onOrAfter": "Şunda veya sonra", - "between": "Şunlar arasında", - "empty": "Boş", - "notEmpty": "Boş değil", + "is": "Tarihinde", + "before": "Öncesinde", + "after": "Sonrasında", + "onOrBefore": "Tarihinde veya öncesinde", + "onOrAfter": "Tarihinde veya sonrasında", + "between": "Arasında", + "empty": "Boştur", + "notEmpty": "Boş değildir", + "startDate": "Başlangıç tarihi", + "endDate": "Bitiş tarihi", "choicechipPrefix": { "before": "Önce", "after": "Sonra", - "onOrBefore": "Şunda veya önce", - "onOrAfter": "Şunda veya sonra", - "isEmpty": "Boş", - "isNotEmpty": "Boş değil" + "between": "Arasında", + "onOrBefore": "Tarihinde veya önce", + "onOrAfter": "Tarihinde veya sonra", + "isEmpty": "Boştur", + "isNotEmpty": "Boş değildir" } }, "numberFilter": { "equal": "Eşittir", "notEqual": "Eşit değildir", - "lessThan": "Şundan küçüktür", - "greaterThan": "Şundan büyüktür", - "lessThanOrEqualTo": "Şundan küçük veya eşittir", - "greaterThanOrEqualTo": "Şundan büyük veya eşittir", - "isEmpty": "Boş", - "isNotEmpty": "Boş değil" + "lessThan": "Küçüktür", + "greaterThan": "Büyüktür", + "lessThanOrEqualTo": "Küçük veya eşittir", + "greaterThanOrEqualTo": "Büyük veya eşittir", + "isEmpty": "Boştur", + "isNotEmpty": "Boş değildir" }, "field": { - "hide": "Gizle", - "show": "Göster", - "insertLeft": "Sola Ekle", - "insertRight": "Sağa Ekle", - "duplicate": "Kopyala", + "label": "Özellik", + "hide": "Özelliği gizle", + "show": "Özelliği göster", + "insertLeft": "Sola ekle", + "insertRight": "Sağa ekle", + "duplicate": "Çoğalt", "delete": "Sil", "wrapCellContent": "Metni kaydır", "clear": "Hücreleri temizle", + "switchPrimaryFieldTooltip": "Birincil alanın alan türü değiştirilemez", "textFieldName": "Metin", - "checkboxFieldName": "Onay Kutusu", + "checkboxFieldName": "Onay kutusu", "dateFieldName": "Tarih", - "updatedAtFieldName": "Son Değiştirilme", - "createdAtFieldName": "Oluşturulma Tarihi", + "updatedAtFieldName": "Son değiştirilme", + "createdAtFieldName": "Oluşturulma tarihi", "numberFieldName": "Sayılar", - "singleSelectFieldName": "Seçenek", - "multiSelectFieldName": "Çoklu Seçenek", + "singleSelectFieldName": "Seçim", + "multiSelectFieldName": "Çoklu seçim", "urlFieldName": "URL", - "checklistFieldName": "Kontrol Listesi", + "checklistFieldName": "Kontrol listesi", "relationFieldName": "İlişki", "summaryFieldName": "Yapay Zeka Özeti", - "timeFieldName": "Zaman", - "translateFieldName": "Yapay Zeka Çevirisi", - "translateTo": "Şuna çevir", - "numberFormat": "Sayı formatı", - "dateFormat": "Tarih formatı", + "timeFieldName": "Saat", + "mediaFieldName": "Dosyalar ve medya", + "translateFieldName": "Yapay Zeka Çeviri", + "translateTo": "Şu dile çevir", + "numberFormat": "Sayı biçimi", + "dateFormat": "Tarih biçimi", "includeTime": "Saati dahil et", "isRange": "Bitiş tarihi", "dateFormatFriendly": "Ay Gün, Yıl", @@ -1305,15 +1500,15 @@ "dateFormatLocal": "Ay/Gün/Yıl", "dateFormatUS": "Yıl/Ay/Gün", "dateFormatDayMonthYear": "Gün/Ay/Yıl", - "timeFormat": "Saat formatı", - "invalidTimeFormat": "Geçersiz format", + "timeFormat": "Saat biçimi", + "invalidTimeFormat": "Geçersiz biçim", "timeFormatTwelveHour": "12 saat", "timeFormatTwentyFourHour": "24 saat", "clearDate": "Tarihi temizle", "dateTime": "Tarih saat", "startDateTime": "Başlangıç tarih saati", "endDateTime": "Bitiş tarih saati", - "failedToLoadDate": "Tarih bilgisi yüklenemedi", + "failedToLoadDate": "Tarih değeri yüklenemedi", "selectTime": "Saat seç", "selectDate": "Tarih seç", "visibility": "Görünürlük", @@ -1324,15 +1519,16 @@ "addOption": "Seçenek ekle", "editProperty": "Özelliği düzenle", "newProperty": "Yeni özellik", - "deleteFieldPromptMessage": "Emin misiniz? Bu özellik silinecek", + "openRowDocument": "Sayfa olarak aç", + "deleteFieldPromptMessage": "Emin misiniz? Bu özellik ve tüm verileri silinecek", "clearFieldPromptMessage": "Emin misiniz? Bu sütundaki tüm hücreler boşaltılacak", - "newColumn": "Yeni Sütun", - "format": "Format", - "reminderOnDateTooltip": "Bu hücrenin planlanmış bir hatırlatıcısı var", + "newColumn": "Yeni sütun", + "format": "Biçim", + "reminderOnDateTooltip": "Bu hücrede planlanmış bir hatırlatıcı var", "optionAlreadyExist": "Seçenek zaten mevcut" }, "rowPage": { - "newField": "Yeni bir alan ekle", + "newField": "Yeni alan ekle", "fieldDragElementTooltip": "Menüyü açmak için tıklayın", "showHiddenFields": { "one": "{count} gizli alanı göster", @@ -1345,36 +1541,42 @@ "other": "{count} gizli alanı gizle" }, "openAsFullPage": "Tam sayfa olarak aç", - "moreRowActions": "Daha fazla satır eylemi" + "moreRowActions": "Daha fazla satır işlemi" }, "sort": { "ascending": "Artan", "descending": "Azalan", - "by": "Şuna göre", + "by": "Göre", "empty": "Aktif sıralama yok", - "cannotFindCreatableField": "Sıralama yapmak için uygun bir alan bulunamadı", + "cannotFindCreatableField": "Sıralanacak uygun bir alan bulunamadı", "deleteAllSorts": "Tüm sıralamaları sil", - "addSort": "Yeni sıralama ekle", - "removeSorting": "Sıralamayı kaldırmak ister misiniz?", - "fieldInUse": "Zaten bu alana göre sıralama yapıyorsunuz" + "addSort": "Sıralama ekle", + "sortsActive": "Sıralama yaparken {intention} yapılamaz", + "removeSorting": "Bu görünümdeki tüm sıralamaları kaldırıp devam etmek istiyor musunuz?", + "fieldInUse": "Bu alana göre zaten sıralama yapıyorsunuz" }, "row": { - "duplicate": "Kopyala", + "label": "Satır", + "duplicate": "Çoğalt", "delete": "Sil", - "titlePlaceholder": "İsimsiz", + "titlePlaceholder": "Başlıksız", "textPlaceholder": "Boş", "copyProperty": "Özellik panoya kopyalandı", "count": "Sayı", "newRow": "Yeni satır", - "action": "Eylem", - "add": "Alta eklemek için tıklayın", + "loadMore": "Daha fazla yükle", + "action": "İşlem", + "add": "Aşağıya eklemek için tıklayın", "drag": "Taşımak için sürükleyin", - "deleteRowPrompt": "Bu satırı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz", - "deleteCardPrompt": "Bu kartı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz", + "deleteRowPrompt": "Bu satırı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "deleteCardPrompt": "Bu kartı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "dragAndClick": "Taşımak için sürükleyin, menüyü açmak için tıklayın", "insertRecordAbove": "Üste kayıt ekle", "insertRecordBelow": "Alta kayıt ekle", - "noContent": "İçerik yok" + "noContent": "İçerik yok", + "reorderRowDescription": "satırı yeniden sırala", + "createRowAboveDescription": "üste bir satır oluştur", + "createRowBelowDescription": "alta bir satır ekle" }, "selectOption": { "create": "Oluştur", @@ -1383,15 +1585,15 @@ "lightPinkColor": "Açık Pembe", "orangeColor": "Turuncu", "yellowColor": "Sarı", - "limeColor": "Limoni", + "limeColor": "Limon", "greenColor": "Yeşil", - "aquaColor": "Su yeşili", + "aquaColor": "Su Mavisi", "blueColor": "Mavi", "deleteTag": "Etiketi sil", "colorPanelTitle": "Renk", - "panelTitle": "Bir seçenek seçin veya yeni bir tane oluşturun", + "panelTitle": "Bir seçenek seçin veya oluşturun", "searchOption": "Bir seçenek arayın", - "searchOrCreateOption": "Bir seçenek arayın veya yeni bir tane oluşturun", + "searchOrCreateOption": "Bir seçenek arayın veya oluşturun", "createNew": "Yeni oluştur", "orSelectOne": "Veya bir seçenek seçin", "typeANewOption": "Yeni bir seçenek yazın", @@ -1399,7 +1601,7 @@ }, "checklist": { "taskHint": "Görev açıklaması", - "addNew": "Yeni bir görev ekle", + "addNew": "Yeni görev ekle", "submitNewTask": "Oluştur", "hideComplete": "Tamamlanan görevleri gizle", "showComplete": "Tüm görevleri göster" @@ -1411,39 +1613,48 @@ "copiedNotification": "Panoya kopyalandı!" }, "relation": { - "relatedDatabasePlaceLabel": "İlgili Veritabanı", + "relatedDatabasePlaceLabel": "İlişkili Veritabanı", "relatedDatabasePlaceholder": "Yok", "inRelatedDatabase": "İçinde", "rowSearchTextFieldPlaceholder": "Ara", - "noDatabaseSelected": "Veritabanı seçilmedi, lütfen önce aşağıdaki listeden bir tane seçin:", + "noDatabaseSelected": "Veritabanı seçilmedi, lütfen aşağıdaki listeden önce bir tane seçin:", "emptySearchResult": "Kayıt bulunamadı", - "linkedRowListLabel": "{count} bağlı satır", - "unlinkedRowListLabel": "Başka bir satırı bağla" + "linkedRowListLabel": "{count} bağlantılı satır", + "unlinkedRowListLabel": "Başka bir satır bağla" }, - "menuName": "Kılavuz", + "menuName": "Tablo", "referencedGridPrefix": "Görünümü", "calculate": "Hesapla", "calculationTypeLabel": { "none": "Yok", "average": "Ortalama", - "max": "Maksimum", + "max": "En büyük", "median": "Medyan", - "min": "Minimum", + "min": "En küçük", "sum": "Toplam", "count": "Sayı", "countEmpty": "Boş sayısı", "countEmptyShort": "BOŞ", - "countNonEmpty": "Dolu sayısı", + "countNonEmpty": "Boş olmayan sayısı", "countNonEmptyShort": "DOLU" }, "media": { "rename": "Yeniden adlandır", "download": "İndir", + "expand": "Genişlet", "delete": "Sil", - "addFileOrImage": "Bir dosya, resim veya bağlantı ekleyin", + "moreFilesHint": "+{}", + "addFileOrImage": "Dosya veya bağlantı ekle", + "attachmentsHint": "{}", "addFileMobile": "Dosya ekle", - "downloadSuccess": "Dosya başarıyla kaydedildi", - "hideFileNames": "Dosya adlarını gizle" + "extraCount": "+{}", + "deleteFileDescription": "Bu dosyayı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "showFileNames": "Dosya adını göster", + "downloadSuccess": "Dosya indirildi", + "downloadFailedToken": "Dosya indirilemedi, kullanıcı jetonu mevcut değil", + "setAsCover": "Kapak olarak ayarla", + "openInBrowser": "Tarayıcıda aç", + "embedLink": "Dosya bağlantısını yerleştir" } }, "document": { @@ -1452,21 +1663,67 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, + "creating": "Oluşturuluyor...", "slashMenu": { "board": { - "selectABoardToLinkTo": "Bağlantı kurulacak bir pano seçin", - "createANewBoard": "Yeni bir pano oluşturun" + "selectABoardToLinkTo": "Bağlanacak bir Pano seçin", + "createANewBoard": "Yeni bir Pano oluştur" }, "grid": { - "selectAGridToLinkTo": "Bağlantı kurulacak bir kılavuz seçin", - "createANewGrid": "Yeni bir kılavuz oluşturun" + "selectAGridToLinkTo": "Bağlanacak bir Tablo seçin", + "createANewGrid": "Yeni bir Tablo oluştur" }, "calendar": { - "selectACalendarToLinkTo": "Bağlantı kurulacak bir takvim seçin", - "createANewCalendar": "Yeni bir takvim oluşturun" + "selectACalendarToLinkTo": "Bağlanacak bir Takvim seçin", + "createANewCalendar": "Yeni bir Takvim oluştur" }, "document": { - "selectADocumentToLinkTo": "Bağlantı kurulacak bir belge seçin" + "selectADocumentToLinkTo": "Bağlanacak bir Belge seçin" + }, + "name": { + "text": "Metin", + "heading1": "Başlık 1", + "heading2": "Başlık 2", + "heading3": "Başlık 3", + "image": "Görsel", + "bulletedList": "Madde işaretli liste", + "numberedList": "Numaralı liste", + "todoList": "Yapılacaklar listesi", + "doc": "Belge", + "linkedDoc": "Sayfaya bağlantı", + "grid": "Tablo", + "linkedGrid": "Bağlantılı Tablo", + "kanban": "Kanban", + "linkedKanban": "Bağlantılı Kanban", + "calendar": "Takvim", + "linkedCalendar": "Bağlantılı Takvim", + "quote": "Alıntı", + "divider": "Ayırıcı", + "table": "Tablo", + "callout": "Not Kutusu", + "outline": "Ana Hat", + "mathEquation": "Matematik Denklemi", + "code": "Kod", + "toggleList": "Açılır liste", + "toggleHeading1": "Açılır başlık 1", + "toggleHeading2": "Açılır başlık 2", + "toggleHeading3": "Açılır başlık 3", + "emoji": "Emoji", + "aiWriter": "Yapay Zeka Yazar", + "dateOrReminder": "Tarih veya Hatırlatıcı", + "photoGallery": "Fotoğraf Galerisi", + "file": "Dosya" + }, + "subPage": { + "name": "Belge", + "keyword1": "alt sayfa", + "keyword2": "sayfa", + "keyword3": "alt sayfa", + "keyword4": "sayfa ekle", + "keyword5": "sayfa yerleştir", + "keyword6": "yeni sayfa", + "keyword7": "sayfa oluştur", + "keyword8": "belge" } }, "selectionMenu": { @@ -1474,62 +1731,95 @@ "codeBlock": "Kod Bloğu" }, "plugins": { - "referencedBoard": "Referans Gösterilen Pano", - "referencedGrid": "Referans Gösterilen Kılavuz", - "referencedCalendar": "Referans Gösterilen Takvim", - "referencedDocument": "Referans Gösterilen Belge", + "referencedBoard": "Referans Pano", + "referencedGrid": "Referans Tablo", + "referencedCalendar": "Referans Takvim", + "referencedDocument": "Referans Belge", "autoGeneratorMenuItemName": "Yapay Zeka Yazar", - "autoGeneratorTitleName": "Yapay Zeka: Yapay zekadan istediğinizi yazmasını isteyin...", - "autoGeneratorLearnMore": "Daha fazla bilgi edinin", + "autoGeneratorTitleName": "Yapay Zeka: Yapay zekadan herhangi bir şey yazmasını isteyin...", + "autoGeneratorLearnMore": "Daha fazla bilgi", "autoGeneratorGenerate": "Oluştur", - "autoGeneratorHintText": "Yapay Zeka'ya sorun ...", - "autoGeneratorCantGetOpenAIKey": "Yapay Zeka anahtarı alınamıyor", - "autoGeneratorRewrite": "Yeniden Yaz", - "smartEdit": "Yapay Zeka Asistanları", + "autoGeneratorHintText": "Yapay zekaya sorun ...", + "autoGeneratorCantGetOpenAIKey": "Yapay zeka anahtarı alınamıyor", + "autoGeneratorRewrite": "Yeniden yaz", + "smartEdit": "Yapay Zekaya Sor", "aI": "Yapay Zeka", - "smartEditFixSpelling": "Yazımı Düzelt", + "smartEditFixSpelling": "Yazım ve dilbilgisini düzelt", "warning": "⚠️ Yapay zeka yanıtları yanlış veya yanıltıcı olabilir.", "smartEditSummarize": "Özetle", - "smartEditImproveWriting": "Yazımı Geliştir", - "smartEditMakeLonger": "Daha Uzun Yap", - "smartEditCouldNotFetchResult": "Yapay Zeka'dan yanıt alınamadı", - "smartEditCouldNotFetchKey": "Yapay Zeka anahtarı alınamadı", - "smartEditDisabled": "Ayarlar'da Yapay Zeka'yı bağlayın", - "appflowyAIEditDisabled": "Yapay zeka özelliklerini kullanmak için oturum açın", - "discardResponse": "Yapay zeka yanıtlarını silmek ister misiniz?", + "smartEditImproveWriting": "Yazımı geliştir", + "smartEditMakeLonger": "Daha uzun yap", + "smartEditCouldNotFetchResult": "Yapay zekadan sonuç alınamadı", + "smartEditCouldNotFetchKey": "Yapay zeka anahtarı alınamadı", + "smartEditDisabled": "Ayarlar'dan Yapay Zeka'yı bağlayın", + "appflowyAIEditDisabled": "Yapay Zeka özelliklerini etkinleştirmek için giriş yapın", + "discardResponse": "Yapay Zeka yanıtlarını silmek istiyor musunuz?", "createInlineMathEquation": "Denklem oluştur", - "fonts": "Yazı Tipleri", + "fonts": "Yazı tipleri", "insertDate": "Tarih ekle", "emoji": "Emoji", - "toggleList": "Listeyi değiştir", + "toggleList": "Açılır liste", + "emptyToggleHeading": "Boş açılır başlık {}. İçerik eklemek için tıklayın.", + "emptyToggleList": "Boş açılır liste. İçerik eklemek için tıklayın.", + "emptyToggleHeadingWeb": "Boş açılır başlık {level}. İçerik eklemek için tıklayın", "quoteList": "Alıntı listesi", "numberedList": "Numaralı liste", "bulletedList": "Madde işaretli liste", - "todoList": "Yapılacaklar Listesi", - "callout": "Bilgi Kutusu", + "todoList": "Yapılacaklar listesi", + "callout": "Not Kutusu", + "simpleTable": { + "moreActions": { + "color": "Renk", + "align": "Hizala", + "delete": "Sil", + "duplicate": "Çoğalt", + "insertLeft": "Sola ekle", + "insertRight": "Sağa ekle", + "insertAbove": "Üste ekle", + "insertBelow": "Alta ekle", + "headerColumn": "Başlık sütunu", + "headerRow": "Başlık satırı", + "clearContents": "İçeriği temizle", + "setToPageWidth": "Sayfa genişliğine ayarla", + "distributeColumnsWidth": "Sütunları eşit dağıt", + "duplicateRow": "Satırı çoğalt", + "duplicateColumn": "Sütunu çoğalt", + "textColor": "Metin rengi", + "cellBackgroundColor": "Hücre arka plan rengi", + "duplicateTable": "Tabloyu çoğalt" + }, + "clickToAddNewRow": "Yeni satır eklemek için tıklayın", + "clickToAddNewColumn": "Yeni sütun eklemek için tıklayın", + "clickToAddNewRowAndColumn": "Yeni satır ve sütun eklemek için tıklayın", + "headerName": { + "table": "Tablo", + "alignText": "Metni hizala" + } + }, "cover": { - "changeCover": "Kapak Resmini Değiştir", + "changeCover": "Kapağı Değiştir", "colors": "Renkler", - "images": "Resimler", + "images": "Görseller", "clearAll": "Tümünü Temizle", "abstract": "Soyut", - "addCover": "Kapak Resmi Ekle", - "addLocalImage": "Yerel resim ekle", - "invalidImageUrl": "Geçersiz resim URL'si", - "failedToAddImageToGallery": "Resim galeriye eklenemedi", - "enterImageUrl": "Resim URL'sini girin", + "addCover": "Kapak Ekle", + "addLocalImage": "Yerel görsel ekle", + "invalidImageUrl": "Geçersiz görsel URL'si", + "failedToAddImageToGallery": "Görsel galeriye eklenemedi", + "enterImageUrl": "Görsel URL'si girin", "add": "Ekle", "back": "Geri", "saveToGallery": "Galeriye kaydet", "removeIcon": "Simgeyi kaldır", - "pasteImageUrl": "Resim URL'sini yapıştır", + "removeCover": "Kapağı kaldır", + "pasteImageUrl": "Görsel URL'sini yapıştırın", "or": "VEYA", "pickFromFiles": "Dosyalardan seç", - "couldNotFetchImage": "Resim yüklenemedi", - "imageSavingFailed": "Resim Kaydedilemedi", + "couldNotFetchImage": "Görsel alınamadı", + "imageSavingFailed": "Görsel Kaydedilemedi", "addIcon": "Simge ekle", "changeIcon": "Simgeyi değiştir", - "coverRemoveAlert": "Silindikten sonra kapak resminden kaldırılacaktır.", + "coverRemoveAlert": "Silindikten sonra kapaktan kaldırılacaktır.", "alertDialogConfirmation": "Devam etmek istediğinizden emin misiniz?" }, "mathEquation": { @@ -1540,9 +1830,11 @@ "optionAction": { "click": "Tıkla", "toOpenMenu": " menüyü açmak için", + "drag": "Sürükle", + "toMove": " taşımak için", "delete": "Sil", - "duplicate": "Kopyala", - "turnInto": "Şuna dönüştür", + "duplicate": "Çoğalt", + "turnInto": "Dönüştür", "moveUp": "Yukarı taşı", "moveDown": "Aşağı taşı", "color": "Renk", @@ -1551,141 +1843,181 @@ "center": "Orta", "right": "Sağ", "defaultColor": "Varsayılan", - "depth": "Derinlik" + "depth": "Derinlik", + "copyLinkToBlock": "Bloğa bağlantıyı kopyala" }, "image": { - "addAnImage": "Bir resim ekle", - "copiedToPasteBoard": "Resim bağlantısı panoya kopyalandı", - "addAnImageDesktop": "Resim(ler)i bırakın veya resim(ler)i eklemek için tıklayın", - "addAnImageMobile": "Bir veya daha fazla resim eklemek için tıklayın", - "dropImageToInsert": "Eklemek için resimleri bırakın", - "imageUploadFailed": "Resim yükleme başarısız oldu", - "imageDownloadFailed": "Resim yükleme başarısız oldu, lütfen tekrar deneyin", - "imageDownloadFailedToken": "Eksik kullanıcı jetonu nedeniyle resim yükleme başarısız oldu, lütfen tekrar deneyin", + "addAnImage": "Görsel ekle", + "copiedToPasteBoard": "Görsel bağlantısı panoya kopyalandı", + "addAnImageDesktop": "Bir görsel ekle", + "addAnImageMobile": "Bir veya daha fazla görsel eklemek için tıklayın", + "dropImageToInsert": "Eklemek için görselleri bırakın", + "imageUploadFailed": "Görsel yüklenemedi", + "imageDownloadFailed": "Görsel indirilemedi, lütfen tekrar deneyin", + "imageDownloadFailedToken": "Kullanıcı jetonu eksik olduğu için görsel indirilemedi, lütfen tekrar deneyin", "errorCode": "Hata kodu" }, "photoGallery": { "name": "Fotoğraf galerisi", - "imageKeyword": "resim", - "imageGalleryKeyword": "resim galerisi", + "imageKeyword": "görsel", + "imageGalleryKeyword": "görsel galerisi", "photoKeyword": "fotoğraf", - "photoBrowserKeyword": "fotoğraf tarayıcı", + "photoBrowserKeyword": "fotoğraf tarayıcısı", "galleryKeyword": "galeri", - "addImageTooltip": "Resim ekle" + "addImageTooltip": "Görsel ekle", + "changeLayoutTooltip": "Düzeni değiştir", + "browserLayout": "Tarayıcı", + "gridLayout": "Izgara", + "deleteBlockTooltip": "Tüm galeriyi sil" }, "math": { "copiedToPasteBoard": "Matematik denklemi panoya kopyalandı" }, "urlPreview": { "copiedToPasteBoard": "Bağlantı panoya kopyalandı", - "convertToLink": "Gömülü bağlantıya dönüştür" + "convertToLink": "Yerleşik bağlantıya dönüştür" }, "outline": { "addHeadingToCreateOutline": "İçindekiler tablosu oluşturmak için başlıklar ekleyin.", "noMatchHeadings": "Eşleşen başlık bulunamadı." }, "table": { - "addAfter": "Sonra ekle", - "addBefore": "Önce ekle", + "addAfter": "Sonrasına ekle", + "addBefore": "Öncesine ekle", "delete": "Sil", "clear": "İçeriği temizle", - "duplicate": "Kopyala", + "duplicate": "Çoğalt", "bgColor": "Arka plan rengi" }, "contextMenu": { "copy": "Kopyala", "cut": "Kes", - "paste": "Yapıştır" + "paste": "Yapıştır", + "pasteAsPlainText": "Düz metin olarak yapıştır" }, - "action": "Eylemler", + "action": "İşlemler", "database": { - "selectDataSource": "Veri kaynağını seçin", + "selectDataSource": "Veri kaynağı seç", "noDataSource": "Veri kaynağı yok", - "selectADataSource": "Bir veri kaynağı seçin", + "selectADataSource": "Bir veri kaynağı seç", "toContinue": "devam etmek için", "newDatabase": "Yeni Veritabanı", - "linkToDatabase": "Veritabanına Bağla" + "linkToDatabase": "Veritabanına Bağlantı" }, "date": "Tarih", "video": { "label": "Video", - "emptyLabel": "Bir video ekleyin", + "emptyLabel": "Video ekle", "placeholder": "Video bağlantısını yapıştırın", "copiedToPasteBoard": "Video bağlantısı panoya kopyalandı", "insertVideo": "Video ekle", - "invalidVideoUrl": "Kaynak URL henüz desteklenmiyor.", + "invalidVideoUrl": "Kaynak URL'si henüz desteklenmiyor.", "invalidVideoUrlYouTube": "YouTube henüz desteklenmiyor.", "supportedFormats": "Desteklenen formatlar: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - } + }, + "file": { + "name": "Dosya", + "uploadTab": "Yükle", + "uploadMobile": "Bir dosya seç", + "uploadMobileGallery": "Fotoğraf Galerisinden", + "networkTab": "Bağlantı yerleştir", + "placeholderText": "Bir dosya yükleyin veya yerleştirin", + "placeholderDragging": "Yüklemek için dosyayı bırakın", + "dropFileToUpload": "Yüklemek için bir dosya bırakın", + "fileUploadHint": "Bir dosya sürükleyip bırakın veya tıklayarak ", + "fileUploadHintSuffix": "Göz atın", + "networkHint": "Bir dosya bağlantısı yapıştırın", + "networkUrlInvalid": "Geçersiz URL. URL'yi kontrol edip tekrar deneyin.", + "networkAction": "Yerleştir", + "fileTooBigError": "Dosya boyutu çok büyük, lütfen 10MB'dan küçük bir dosya yükleyin", + "renameFile": { + "title": "Dosyayı yeniden adlandır", + "description": "Bu dosya için yeni bir ad girin", + "nameEmptyError": "Dosya adı boş bırakılamaz." + }, + "uploadedAt": "{} tarihinde yüklendi", + "linkedAt": "Bağlantısı {} tarihinde eklendi", + "failedToOpenMsg": "Açılamadı, dosya bulunamadı" + }, + "subPage": { + "handlingPasteHint": " - (yapıştırma işlemi)", + "errors": { + "failedDeletePage": "Sayfa silinemedi", + "failedCreatePage": "Sayfa oluşturulamadı", + "failedMovePage": "Sayfa bu belgeye taşınamadı", + "failedDuplicatePage": "Sayfa çoğaltılamadı", + "failedDuplicateFindView": "Sayfa çoğaltılamadı - orijinal görünüm bulunamadı" + } + }, + "cannotMoveToItsChildren": "Alt öğelerine taşınamaz" }, "outlineBlock": { "placeholder": "İçindekiler" }, "textBlock": { - "placeholder": "Komutlar için '/' yazın veya yazmaya başlayın" + "placeholder": "Komutlar için '/' yazın" }, "title": { - "placeholder": "İsimsiz" + "placeholder": "Başlıksız" }, "imageBlock": { - "placeholder": "Resim(ler)i eklemek için tıklayın", + "placeholder": "Görsel eklemek için tıklayın", "upload": { "label": "Yükle", - "placeholder": "Resim yüklemek için tıklayın" + "placeholder": "Görsel yüklemek için tıklayın" }, "url": { - "label": "Resim URL'si", - "placeholder": "Resim URL'sini girin" + "label": "Görsel URL'si", + "placeholder": "Görsel URL'si girin" }, "ai": { - "label": "Yapay Zeka'dan resim oluştur", - "placeholder": "Lütfen Yapay Zeka'nın resim oluşturması için komutu girin" + "label": "Yapay zeka ile görsel oluştur", + "placeholder": "Yapay zekanın görsel oluşturması için bir istek girin" }, "stability_ai": { - "label": "Stability Yapay Zeka ile resim oluştur", - "placeholder": "Lütfen Stability Yapay Zeka'nın resim oluşturması için komutu girin" + "label": "Stability AI ile görsel oluştur", + "placeholder": "Stability AI'nın görsel oluşturması için bir istek girin" }, - "support": "Resim boyutu sınırı 5 MB'dir. Desteklenen formatlar: JPEG, PNG, GIF, SVG", + "support": "Görsel boyut sınırı 5MB'dır. Desteklenen formatlar: JPEG, PNG, GIF, SVG", "error": { - "invalidImage": "Geçersiz resim", - "invalidImageSize": "Resim boyutu 5 MB'den küçük olmalıdır", - "invalidImageFormat": "Resim formatı desteklenmiyor. Desteklenen formatlar: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "Geçersiz resim URL'si", + "invalidImage": "Geçersiz görsel", + "invalidImageSize": "Görsel boyutu 5MB'dan küçük olmalıdır", + "invalidImageFormat": "Görsel formatı desteklenmiyor. Desteklenen formatlar: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "Geçersiz görsel URL'si", "noImage": "Böyle bir dosya veya dizin yok", - "multipleImagesFailed": "Bir veya daha fazla resim yüklenemedi, lütfen tekrar deneyin" + "multipleImagesFailed": "Bir veya daha fazla görsel yüklenemedi, lütfen tekrar deneyin" }, "embedLink": { - "label": "Bağlantıyı göm", - "placeholder": "Bir resim bağlantısını yapıştırın veya yazın" + "label": "Bağlantı yerleştir", + "placeholder": "Bir görsel bağlantısı yapıştırın veya yazın" }, "unsplash": { "label": "Unsplash" }, - "searchForAnImage": "Bir resim arayın", - "pleaseInputYourOpenAIKey": "Lütfen Ayarlar sayfasında Yapay Zeka anahtarınızı girin", - "saveImageToGallery": "Resmi kaydet", - "failedToAddImageToGallery": "Resim galeriye eklenemedi", - "successToAddImageToGallery": "Resim başarıyla galeriye eklendi", - "unableToLoadImage": "Resim yüklenemedi", - "maximumImageSize": "Desteklenen maksimum yükleme resim boyutu 10 MB'dir", - "uploadImageErrorImageSizeTooBig": "Resim boyutu 10 MB'den küçük olmalıdır", - "imageIsUploading": "Resim yükleniyor", - "openFullScreen": "Tam ekranda aç", + "searchForAnImage": "Bir görsel ara", + "pleaseInputYourOpenAIKey": "lütfen Ayarlar sayfasından yapay zeka anahtarınızı girin", + "saveImageToGallery": "Görseli kaydet", + "failedToAddImageToGallery": "Görsel galeriye eklenemedi", + "successToAddImageToGallery": "Görsel başarıyla galeriye eklendi", + "unableToLoadImage": "Görsel yüklenemedi", + "maximumImageSize": "Desteklenen maksimum görsel yükleme boyutu 10MB'dır", + "uploadImageErrorImageSizeTooBig": "Görsel boyutu 10MB'dan küçük olmalıdır", + "imageIsUploading": "Görsel yükleniyor", + "openFullScreen": "Tam ekran aç", "interactiveViewer": { "toolbar": { - "previousImageTooltip": "Önceki resim", - "nextImageTooltip": "Sonraki resim", + "previousImageTooltip": "Önceki görsel", + "nextImageTooltip": "Sonraki görsel", "zoomOutTooltip": "Uzaklaştır", "zoomInTooltip": "Yakınlaştır", "changeZoomLevelTooltip": "Yakınlaştırma seviyesini değiştir", - "openLocalImage": "Resmi aç", - "downloadImage": "Resmi indir", + "openLocalImage": "Görseli aç", + "downloadImage": "Görseli indir", "closeViewer": "Etkileşimli görüntüleyiciyi kapat", "scalePercentage": "%{}", - "deleteImageTooltip": "Resmi sil" + "deleteImageTooltip": "Görseli sil" } - }, - "pleaseInputYourStabilityAIKey": "Lütfen Ayarlar sayfasında Stability Yapay Zeka anahtarınızı girin" + } }, "codeBlock": { "language": { @@ -1693,59 +2025,71 @@ "placeholder": "Dil seçin", "auto": "Otomatik" }, - "copyTooltip": "Kod bloğunun içeriğini kopyala", + "copyTooltip": "Kopyala", "searchLanguageHint": "Bir dil arayın", "codeCopiedSnackbar": "Kod panoya kopyalandı!" }, "inlineLink": { - "placeholder": "Bir bağlantıyı yapıştırın veya yazın", + "placeholder": "Bir bağlantı yapıştırın veya yazın", "openInNewTab": "Yeni sekmede aç", "copyLink": "Bağlantıyı kopyala", "removeLink": "Bağlantıyı kaldır", "url": { "label": "Bağlantı URL'si", - "placeholder": "Bağlantı URL'sini girin" + "placeholder": "Bağlantı URL'si girin" }, "title": { "label": "Bağlantı Başlığı", - "placeholder": "Bağlantı başlığını girin" + "placeholder": "Bağlantı başlığı girin" } }, "mention": { - "placeholder": "Bir kişiye, sayfaya veya tarihe bahset...", + "placeholder": "Bir kişiden, sayfadan veya tarihten bahsedin...", "page": { "label": "Sayfaya bağlantı", "tooltip": "Sayfayı açmak için tıklayın" }, "deleted": "Silindi", - "deletedContent": "Bu içerik mevcut değil veya silinmiş" + "deletedContent": "Bu içerik mevcut değil veya silinmiş", + "noAccess": "Erişim Yok", + "deletedPage": "Silinmiş sayfa", + "trashHint": " - çöp kutusunda", + "morePages": "daha fazla sayfa" }, "toolbar": { "resetToDefaultFont": "Varsayılana sıfırla" }, "errorBlock": { - "theBlockIsNotSupported": "Blok içeriği ayrıştırılamadı", + "theBlockIsNotSupported": "Mevcut sürüm bu Bloğu desteklemiyor.", "clickToCopyTheBlockContent": "Blok içeriğini kopyalamak için tıklayın", - "blockContentHasBeenCopied": "Blok içeriği kopyalandı" + "blockContentHasBeenCopied": "Blok içeriği kopyalandı.", + "parseError": "{} bloğu ayrıştırılırken bir hata oluştu.", + "copyBlockContent": "Blok içeriğini kopyala" }, "mobilePageSelector": { - "title": "Sayfa seçin", + "title": "Sayfa seç", "failedToLoad": "Sayfa listesi yüklenemedi", "noPagesFound": "Sayfa bulunamadı" + }, + "attachmentMenu": { + "choosePhoto": "Fotoğraf seç", + "takePicture": "Fotoğraf çek", + "chooseFile": "Dosya seç" } }, "board": { "column": { + "label": "Sütun", "createNewCard": "Yeni", "renameGroupTooltip": "Grubu yeniden adlandırmak için basın", "createNewColumn": "Yeni bir grup ekle", "addToColumnTopTooltip": "Üste yeni bir kart ekle", "addToColumnBottomTooltip": "Alta yeni bir kart ekle", - "renameColumn": "Yeniden Adlandır", + "renameColumn": "Yeniden adlandır", "hideColumn": "Gizle", "newGroup": "Yeni grup", "deleteColumn": "Sil", - "deleteColumnConfirmation": "Bu, bu grubu ve içindeki tüm kartları silecektir.\nDevam etmek istediğinizden emin misiniz?" + "deleteColumnConfirmation": "Bu işlem bu grubu ve içindeki tüm kartları silecektir. Devam etmek istediğinizden emin misiniz?" }, "hiddenGroupSection": { "sectionTitle": "Gizli Gruplar", @@ -1753,23 +2097,23 @@ "expandTooltip": "Gizli grupları görüntüle" }, "cardDetail": "Kart Detayı", - "cardActions": "Kart Eylemleri", - "cardDuplicated": "Kart kopyalandı", + "cardActions": "Kart İşlemleri", + "cardDuplicated": "Kart çoğaltıldı", "cardDeleted": "Kart silindi", "showOnCard": "Kart detayında göster", "setting": "Ayar", "propertyName": "Özellik adı", "menuName": "Pano", - "showUngrouped": "Grupsuz öğeleri göster", - "ungroupedButtonText": "Grupsuz", + "showUngrouped": "Gruplanmamış öğeleri göster", + "ungroupedButtonText": "Gruplanmamış", "ungroupedButtonTooltip": "Herhangi bir gruba ait olmayan kartları içerir", "ungroupedItemsTitle": "Panoya eklemek için tıklayın", - "groupBy": "Şuna göre grupla", + "groupBy": "Grupla", "groupCondition": "Gruplama koşulu", "referencedBoardPrefix": "Görünümü", - "notesTooltip": "İçindeki notlar", + "notesTooltip": "İçerideki notlar", "mobile": { - "editURL": "URL'yi Düzenle", + "editURL": "URL'yi düzenle", "showGroup": "Grubu göster", "showGroupContent": "Bu grubu panoda göstermek istediğinizden emin misiniz?", "failedToLoad": "Pano görünümü yüklenemedi" @@ -1780,20 +2124,24 @@ "yesterday": "Dün", "tomorrow": "Yarın", "lastSevenDays": "Son 7 gün", - "nextSevenDays": "Sonraki 7 gün", + "nextSevenDays": "Gelecek 7 gün", "lastThirtyDays": "Son 30 gün", - "nextThirtyDays": "Sonraki 30 gün" + "nextThirtyDays": "Gelecek 30 gün" }, - "noGroup": "Gruplama özelliği yok", - "noGroupDesc": "Pano görünümlerinin görüntülenebilmesi için gruplama yapılacak bir özellik gerekir" + "noGroup": "Özelliğe göre gruplama yok", + "noGroupDesc": "Pano görünümleri görüntülemek için gruplamak üzere bir özellik gerektirir", + "media": { + "cardText": "{} {}", + "fallbackName": "dosyalar" + } }, "calendar": { "menuName": "Takvim", - "defaultNewCalendarTitle": "İsimsiz", - "newEventButtonTooltip": "Yeni bir etkinlik ekleyin", + "defaultNewCalendarTitle": "Başlıksız", + "newEventButtonTooltip": "Yeni etkinlik ekle", "navigation": { "today": "Bugün", - "jumpToday": "Bugüne Git", + "jumpToday": "Bugüne git", "previousMonth": "Önceki Ay", "nextMonth": "Sonraki Ay", "views": { @@ -1805,14 +2153,14 @@ }, "mobileEventScreen": { "emptyTitle": "Henüz etkinlik yok", - "emptyBody": "Bu güne bir etkinlik oluşturmak için artı düğmesine basın." + "emptyBody": "Bu güne etkinlik eklemek için artı düğmesine basın." }, "settings": { "showWeekNumbers": "Hafta numaralarını göster", "showWeekends": "Hafta sonlarını göster", - "firstDayOfWeek": "Haftayı şunda başlat", + "firstDayOfWeek": "Haftanın başlangıç günü", "layoutDateField": "Takvimi şuna göre düzenle", - "changeLayoutDateField": "Düzenleme alanını değiştir", + "changeLayoutDateField": "Düzen alanını değiştir", "noDateTitle": "Tarih Yok", "noDateHint": { "zero": "Planlanmamış etkinlikler burada görünecek", @@ -1826,15 +2174,18 @@ }, "referencedCalendarPrefix": "Görünümü", "quickJumpYear": "Şuraya git", - "duplicateEvent": "Etkinliği kopyala" + "duplicateEvent": "Etkinliği çoğalt" }, "errorDialog": { "title": "@:appName Hatası", - "howToFixFallback": "Verdiğimiz rahatsızlıktan dolayı özür dileriz! Lütfen GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", - "github": "GitHub'da Görüntüle" + "howToFixFallback": "Rahatsızlık için özür dileriz! GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", + "howToFixFallbackHint1": "Rahatsızlık için özür dileriz! ", + "howToFixFallbackHint2": " sayfamızda hatanızı açıklayan bir sorun bildirin.", + "github": "GitHub'da görüntüle" }, "search": { "label": "Ara", + "sidebarSearchIcon": "Ara ve hızlıca bir sayfaya git", "placeholder": { "actions": "Eylemleri ara..." } @@ -1845,7 +2196,7 @@ "fail": "Kopyalanamadı" } }, - "unSupportBlock": "Geçerli sürüm bu bloğu desteklemiyor.", + "unSupportBlock": "Mevcut sürüm bu Bloğu desteklemiyor.", "views": { "deleteContentTitle": "{pageType} silmek istediğinizden emin misiniz?", "deleteContentCaption": "Bu {pageType} silerseniz, çöp kutusundan geri yükleyebilirsiniz." @@ -1868,22 +2219,22 @@ "search": "Emoji ara", "noRecent": "Son kullanılan emoji yok", "noEmojiFound": "Emoji bulunamadı", - "filter": "Filtre", + "filter": "Filtrele", "random": "Rastgele", - "selectSkinTone": "Cilt tonunu seç", + "selectSkinTone": "Ten rengi seç", "remove": "Emojiyi kaldır", "categories": { - "smileys": "Gülen Yüzler & Duygular", - "people": "İnsanlar & Vücut", - "animals": "Hayvanlar & Doğa", - "food": "Yiyecek & İçecek", - "activities": "Aktiviteler", - "places": "Seyahat & Yerler", - "objects": "Nesneler", - "symbols": "Semboller", - "flags": "Bayraklar", - "nature": "Doğa", - "frequentlyUsed": "Sık Kullanılanlar" + "smileys": "İfadeler ve Duygular", + "people": "insanlar", + "animals": "doğa", + "food": "yiyecekler", + "activities": "aktiviteler", + "places": "yerler", + "objects": "nesneler", + "symbols": "semboller", + "flags": "bayraklar", + "nature": "doğa", + "frequentlyUsed": "sık kullanılanlar" }, "skinTone": { "default": "Varsayılan", @@ -1892,7 +2243,8 @@ "medium": "Orta", "mediumDark": "Orta-Koyu", "dark": "Koyu" - } + }, + "openSourceIconsFrom": "Açık kaynak ikonlar" }, "inlineActions": { "noResults": "Sonuç yok", @@ -1901,25 +2253,26 @@ "docReference": "Belge referansı", "boardReference": "Pano referansı", "calReference": "Takvim referansı", - "gridReference": "Kılavuz referansı", + "gridReference": "Tablo referansı", "date": "Tarih", "reminder": { "groupTitle": "Hatırlatıcı", "shortKeyword": "hatırlat" - } + }, + "createPage": "\"{}\"-alt sayfası oluştur" }, "datePicker": { - "dateTimeFormatTooltip": "Ayarlardan tarih ve saat formatını değiştirin", - "dateFormat": "Tarih formatı", + "dateTimeFormatTooltip": "Tarih ve saat biçimini ayarlardan değiştirin", + "dateFormat": "Tarih biçimi", "includeTime": "Saati dahil et", "isRange": "Bitiş tarihi", - "timeFormat": "Saat formatı", + "timeFormat": "Saat biçimi", "clearDate": "Tarihi temizle", "reminderLabel": "Hatırlatıcı", "selectReminder": "Hatırlatıcı seç", "reminderOptions": { "none": "Yok", - "atTimeOfEvent": "Etkinlik zamanı", + "atTimeOfEvent": "Etkinlik zamanında", "fiveMinsBefore": "5 dakika önce", "tenMinsBefore": "10 dakika önce", "fifteenMinsBefore": "15 dakika önce", @@ -1944,8 +2297,8 @@ "mobile": { "title": "Güncellemeler" }, - "emptyTitle": "Her şey tamam!", - "emptyBody": "Bekleyen bildirim veya eylem yok. Sakinliğin tadını çıkarın.", + "emptyTitle": "Hepsi tamamlandı!", + "emptyBody": "Bekleyen bildirim veya eylem yok. Huzurun tadını çıkarın.", "tabs": { "inbox": "Gelen Kutusu", "upcoming": "Yaklaşan" @@ -1959,16 +2312,16 @@ "ascending": "Artan", "descending": "Azalan", "groupByDate": "Tarihe göre grupla", - "showUnreadsOnly": "Yalnızca okunmamışları göster", + "showUnreadsOnly": "Sadece okunmamışları göster", "resetToDefault": "Varsayılana sıfırla" } }, "reminderNotification": { "title": "Hatırlatıcı", - "message": "Unutmadan bir göz atın!", + "message": "Unutmadan önce bunu kontrol etmeyi unutmayın!", "tooltipDelete": "Sil", "tooltipMarkRead": "Okundu olarak işaretle", - "tooltipMarkUnread": "Okunmamış olarak işaretle" + "tooltipMarkUnread": "Okunmadı olarak işaretle" }, "findAndReplace": { "find": "Bul", @@ -1979,34 +2332,40 @@ "replaceAll": "Tümünü değiştir", "noResult": "Sonuç yok", "caseSensitive": "Büyük/küçük harf duyarlı", - "searchMore": "Daha fazla sonuç bulmak için ara" + "searchMore": "Daha fazla sonuç bulmak için arama yapın" }, "error": { "weAreSorry": "Üzgünüz", - "loadingViewError": "Bu görünümü yüklemede sorun yaşıyoruz. Lütfen internet bağlantınızı kontrol edin, uygulamayı yenileyin ve sorun devam ederse ekibe ulaşmaktan çekinmeyin." + "loadingViewError": "Bu görünümü yüklerken sorun yaşıyoruz. Lütfen internet bağlantınızı kontrol edin, uygulamayı yenileyin ve sorun devam ederse ekiple iletişime geçmekten çekinmeyin.", + "syncError": "Veriler başka bir cihazdan senkronize edilmedi", + "syncErrorHint": "Lütfen bu sayfayı son düzenlemenin yapıldığı cihazda yeniden açın, ardından mevcut cihazda tekrar açın.", + "clickToCopy": "Hata kodunu kopyalamak için tıklayın" }, "editor": { "bold": "Kalın", - "bulletedList": "Madde İşaretli Liste", - "bulletedListShortForm": "Madde İşaretli", - "checkbox": "Onay Kutusu", - "embedCode": "Kodu Göm", + "bulletedList": "Madde işaretli liste", + "bulletedListShortForm": "Madde işaretli", + "checkbox": "Onay kutusu", + "embedCode": "Kod Yerleştir", "heading1": "H1", "heading2": "H2", "heading3": "H3", "highlight": "Vurgula", "color": "Renk", - "image": "Resim", + "image": "Görsel", "date": "Tarih", "page": "Sayfa", "italic": "İtalik", "link": "Bağlantı", - "numberedList": "Numaralı Liste", + "numberedList": "Numaralı liste", "numberedListShortForm": "Numaralı", + "toggleHeading1ShortForm": "B1'i aç/kapat", + "toggleHeading2ShortForm": "B2'yi aç/kapat", + "toggleHeading3ShortForm": "B3'ü aç/kapat", "quote": "Alıntı", - "strikethrough": "Üstü Çizili", + "strikethrough": "Üstü çizili", "text": "Metin", - "underline": "Altı Çizili", + "underline": "Altı çizili", "fontColorDefault": "Varsayılan", "fontColorGray": "Gri", "fontColorBrown": "Kahverengi", @@ -2027,9 +2386,9 @@ "backgroundColorPurple": "Mor arka plan", "backgroundColorPink": "Pembe arka plan", "backgroundColorRed": "Kırmızı arka plan", - "backgroundColorLime": "Limoni arka plan", - "backgroundColorAqua": "Su yeşili arka plan", - "done": "Bitti", + "backgroundColorLime": "Limon yeşili arka plan", + "backgroundColorAqua": "Su mavisi arka plan", + "done": "Tamam", "cancel": "İptal", "tint1": "Ton 1", "tint2": "Ton 2", @@ -2045,14 +2404,17 @@ "lightLightTint3": "Açık Pembe", "lightLightTint4": "Turuncu", "lightLightTint5": "Sarı", - "lightLightTint6": "Limoni", + "lightLightTint6": "Limon yeşili", "lightLightTint7": "Yeşil", - "lightLightTint8": "Su yeşili", + "lightLightTint8": "Su mavisi", "lightLightTint9": "Mavi", "urlHint": "URL", "mobileHeading1": "Başlık 1", "mobileHeading2": "Başlık 2", "mobileHeading3": "Başlık 3", + "mobileHeading4": "Başlık 4", + "mobileHeading5": "Başlık 5", + "mobileHeading6": "Başlık 6", "textColor": "Metin Rengi", "backgroundColor": "Arka Plan Rengi", "addYourLink": "Bağlantınızı ekleyin", @@ -2063,14 +2425,14 @@ "linkText": "Metin", "linkTextHint": "Lütfen metin girin", "linkAddressHint": "Lütfen URL girin", - "highlightColor": "Vurgu rengi", - "clearHighlightColor": "Vurgu rengini temizle", + "highlightColor": "Vurgulama rengi", + "clearHighlightColor": "Vurgulama rengini temizle", "customColor": "Özel renk", "hexValue": "Hex değeri", "opacity": "Opaklık", "resetToDefaultColor": "Varsayılan renge sıfırla", - "ltr": "LTR", - "rtl": "RTL", + "ltr": "Soldan sağa", + "rtl": "Sağdan sola", "auto": "Otomatik", "cut": "Kes", "copy": "Kopyala", @@ -2083,15 +2445,15 @@ "closeFind": "Kapat", "replace": "Değiştir", "replaceAll": "Tümünü değiştir", - "regex": "Regex", + "regex": "Düzenli ifade", "caseSensitive": "Büyük/küçük harf duyarlı", - "uploadImage": "Resim Yükle", - "urlImage": "URL Resmi", + "uploadImage": "Görsel Yükle", + "urlImage": "URL Görseli", "incorrectLink": "Hatalı Bağlantı", "upload": "Yükle", - "chooseImage": "Bir resim seçin", + "chooseImage": "Bir görsel seçin", "loading": "Yükleniyor", - "imageLoadFailed": "Resim yüklenemedi", + "imageLoadFailed": "Görsel yüklenemedi", "divider": "Ayırıcı", "table": "Tablo", "colAddBefore": "Önce ekle", @@ -2100,25 +2462,25 @@ "rowAddAfter": "Sonra ekle", "colRemove": "Kaldır", "rowRemove": "Kaldır", - "colDuplicate": "Kopyala", - "rowDuplicate": "Kopyala", + "colDuplicate": "Çoğalt", + "rowDuplicate": "Çoğalt", "colClear": "İçeriği Temizle", "rowClear": "İçeriği Temizle", - "slashPlaceHolder": "Bir blok eklemek için '/' yazın veya yazmaya başlayın", + "slashPlaceHolder": "Blok eklemek için '/' yazın veya yazmaya başlayın", "typeSomething": "Bir şeyler yazın...", - "toggleListShortForm": "Liste Değiştir", + "toggleListShortForm": "Aç/Kapat", "quoteListShortForm": "Alıntı", "mathEquationShortForm": "Formül", "codeBlockShortForm": "Kod" }, "favorite": { "noFavorite": "Favori sayfa yok", - "noFavoriteHintText": "Sayfayı favorilerinize eklemek için sola kaydırın", + "noFavoriteHintText": "Favorilerinize eklemek için sayfayı sola kaydırın", "removeFromSidebar": "Kenar çubuğundan kaldır", "addToSidebar": "Kenar çubuğuna sabitle" }, "cardDetails": { - "notesPlaceholder": "Bir blok eklemek için '/' yazın veya yazmaya başlayın" + "notesPlaceholder": "Blok eklemek için / yazın veya yazmaya başlayın" }, "blockPlaceholders": { "todoList": "Yapılacaklar", @@ -2128,52 +2490,60 @@ "heading": "Başlık {}" }, "titleBar": { - "pageIcon": "Sayfa simgesi", + "pageIcon": "Sayfa ikonu", "language": "Dil", - "font": "Yazı Tipi", + "font": "Yazı tipi", "actions": "Eylemler", "date": "Tarih", "addField": "Alan ekle", - "userIcon": "Kullanıcı simgesi" + "userIcon": "Kullanıcı ikonu" }, "noLogFiles": "Günlük dosyası yok", "newSettings": { "myAccount": { "title": "Hesabım", - "subtitle": "Profilinizi özelleştirin, hesap güvenliğini yönetin, Open AI anahtarlarını yönetin veya hesabınıza giriş yapın.", - "profileLabel": "Hesap adı & Profil resmi", + "subtitle": "Profilinizi özelleştirin, hesap güvenliğini yönetin, yapay zeka anahtarlarını ayarlayın veya hesabınıza giriş yapın.", + "profileLabel": "Hesap adı ve Profil resmi", "profileNamePlaceholder": "Adınızı girin", "accountSecurity": "Hesap güvenliği", "2FA": "2 Adımlı Doğrulama", - "aiKeys": "Yapay Zeka anahtarları", + "aiKeys": "Yapay zeka anahtarları", "accountLogin": "Hesap Girişi", "updateNameError": "Ad güncellenemedi", - "updateIconError": "Simge güncellenemedi", + "updateIconError": "İkon güncellenemedi", "deleteAccount": { "title": "Hesabı Sil", "subtitle": "Hesabınızı ve tüm verilerinizi kalıcı olarak silin.", + "description": "Hesabınızı kalıcı olarak silin ve tüm çalışma alanlarından erişimi kaldırın.", "deleteMyAccount": "Hesabımı sil", "dialogTitle": "Hesabı sil", "dialogContent1": "Hesabınızı kalıcı olarak silmek istediğinizden emin misiniz?", - "dialogContent2": "Bu işlem geri alınamaz ve tüm ekip alanlarına erişiminizi kaldıracak, hesabınızı (özel çalışma alanları dahil) tamamen silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır." + "dialogContent2": "Bu işlem GERİ ALINAMAZ ve tüm çalışma alanlarından erişiminizi kaldıracak, özel çalışma alanları dahil tüm hesabınızı silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır.", + "confirmHint1": "Onaylamak için lütfen \"HESABIMI SİL\" yazın.", + "confirmHint2": "Bu işlemin GERİ ALINAMAZ olduğunu ve hesabımı ve ilişkili tüm verileri kalıcı olarak sileceğini anlıyorum.", + "confirmHint3": "HESABIMI SİL", + "checkToConfirmError": "Silme işlemini onaylamak için kutuyu işaretlemelisiniz", + "failedToGetCurrentUser": "Mevcut kullanıcı e-postası alınamadı", + "confirmTextValidationFailed": "Onay metniniz \"HESABIMI SİL\" ile eşleşmiyor", + "deleteAccountSuccess": "Hesap başarıyla silindi" } }, "workplace": { - "name": "Çalışma Alanı", + "name": "Çalışma alanı", "title": "Çalışma Alanı Ayarları", - "subtitle": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarihini, saatini ve dilini özelleştirin.", + "subtitle": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.", "workplaceName": "Çalışma alanı adı", "workplaceNamePlaceholder": "Çalışma alanı adını girin", - "workplaceIcon": "Çalışma alanı simgesi", - "workplaceIconSubtitle": "Çalışma alanınız için bir resim yükleyin veya bir emoji kullanın. Simge, kenar çubuğunuzda ve bildirimlerinizde görünecektir.", + "workplaceIcon": "Çalışma alanı ikonu", + "workplaceIconSubtitle": "Bir görsel yükleyin veya çalışma alanınız için bir emoji kullanın. İkon kenar çubuğunuzda ve bildirimlerde görünecektir.", "renameError": "Çalışma alanı yeniden adlandırılamadı", - "updateIconError": "Simge güncellenemedi", - "chooseAnIcon": "Bir simge seçin", + "updateIconError": "İkon güncellenemedi", + "chooseAnIcon": "Bir ikon seçin", "appearance": { "name": "Görünüm", "themeMode": { "auto": "Otomatik", - "light": "Açık", + "light": "Aydınlık", "dark": "Koyu" }, "language": "Dil" @@ -2188,30 +2558,32 @@ "pageStyle": { "title": "Sayfa stili", "layout": "Düzen", - "coverImage": "Kapak resmi", - "pageIcon": "Sayfa simgesi", + "coverImage": "Kapak görseli", + "pageIcon": "Sayfa ikonu", "colors": "Renkler", "gradient": "Gradyan", - "backgroundImage": "Arka plan resmi", - "presets": "Ön ayarlar", + "backgroundImage": "Arka plan görseli", + "presets": "Hazır ayarlar", "photo": "Fotoğraf", "unsplash": "Unsplash", "pageCover": "Sayfa kapağı", "none": "Yok", "openSettings": "Ayarları Aç", "photoPermissionTitle": "@:appName fotoğraf kitaplığınıza erişmek istiyor", - "photoPermissionDescription": "Görüntü yüklemek için fotoğraf kitaplığına erişim izni verin.", + "photoPermissionDescription": "@:appName belgelerinize görsel ekleyebilmeniz için fotoğraflarınıza erişmeye ihtiyaç duyuyor", + "cameraPermissionTitle": "@:appName kameranıza erişmek istiyor", + "cameraPermissionDescription": "@:appName belgelerinize kameradan görsel ekleyebilmeniz için kameranıza erişmeye ihtiyaç duyuyor", "doNotAllow": "İzin Verme", - "image": "Resim" + "image": "Görsel" }, "commandPalette": { - "placeholder": "Aramak için yazın...", + "placeholder": "Ara veya bir soru sor...", "bestMatches": "En iyi eşleşmeler", "recentHistory": "Son geçmiş", "navigateHint": "gezinmek için", - "loadingTooltip": "Sonuçlar aranıyor...", + "loadingTooltip": "Sonuçları arıyoruz...", "betaLabel": "BETA", - "betaTooltip": "Şu anda yalnızca sayfaları ve belgelerdeki içeriği aramayı destekliyoruz", + "betaTooltip": "Şu anda yalnızca sayfalarda ve belgelerde içerik aramayı destekliyoruz", "fromTrashHint": "Çöp kutusundan", "noResultsHint": "Aradığınızı bulamadık, başka bir terim aramayı deneyin.", "clearSearchTooltip": "Arama alanını temizle" @@ -2219,26 +2591,26 @@ "space": { "delete": "Sil", "deleteConfirmation": "Sil: ", - "deleteConfirmationDescription": "Bu Alan içindeki tüm sayfalar silinecek ve Çöp Kutusu'na taşınacaktır. Yayınlanan sayfalar da silinecektir.", + "deleteConfirmationDescription": "Bu Alan içindeki tüm sayfalar silinecek ve Çöp Kutusuna taşınacak, yayınlanmış sayfaların yayını kaldırılacaktır.", "rename": "Alanı Yeniden Adlandır", - "changeIcon": "Simgeyi değiştir", + "changeIcon": "İkonu değiştir", "manage": "Alanı Yönet", "addNewSpace": "Alan Oluştur", "collapseAllSubPages": "Tüm alt sayfaları daralt", "createNewSpace": "Yeni bir alan oluştur", - "createSpaceDescription": "Çalışmanızı daha iyi organize etmek için birden fazla genel ve özel alan oluşturun.", + "createSpaceDescription": "İşlerinizi daha iyi organize etmek için birden fazla genel ve özel alan oluşturun.", "spaceName": "Alan adı", - "spaceNamePlaceholder": "ör. Pazarlama, Mühendislik, İK", - "permission": "İzin", + "spaceNamePlaceholder": "örn. Pazarlama, Mühendislik, İK", + "permission": "Alan izni", "publicPermission": "Genel", "publicPermissionDescription": "Tam erişime sahip tüm çalışma alanı üyeleri", "privatePermission": "Özel", - "privatePermissionDescription": "Yalnızca siz bu alana erişebilirsiniz", + "privatePermissionDescription": "Bu alana yalnızca siz erişebilirsiniz", "spaceIconBackground": "Arka plan rengi", - "spaceIcon": "Simge", - "dangerZone": "Tehlike Bölgesi", - "unableToDeleteLastSpace": "Son Alan silinemiyor", - "unableToDeleteSpaceNotCreatedByYou": "Başkaları tarafından oluşturulan Alanlar silinemiyor", + "spaceIcon": "İkon", + "dangerZone": "Tehlikeli Bölge", + "unableToDeleteLastSpace": "Son Alan silinemez", + "unableToDeleteSpaceNotCreatedByYou": "Başkaları tarafından oluşturulan alanlar silinemez", "enableSpacesForYourWorkspace": "Çalışma alanınız için Alanları etkinleştirin", "title": "Alanlar", "defaultSpaceName": "Genel", @@ -2246,53 +2618,468 @@ "upgradeSpaceDescription": "Çalışma alanınızı daha iyi organize etmek için birden fazla genel ve özel Alan oluşturun.", "upgrade": "Güncelle", "upgradeYourSpace": "Birden fazla Alan oluştur", - "quicklySwitch": "Hızlıca bir sonraki alana geç", - "duplicate": "Alanı Kopyala", + "quicklySwitch": "Hızlıca sonraki alana geç", + "duplicate": "Alanı Çoğalt", "movePageToSpace": "Sayfayı alana taşı", - "switchSpace": "Alanı değiştir" + "cannotMovePageToDatabase": "Sayfa veritabanına taşınamaz", + "switchSpace": "Alan değiştir", + "spaceNameCannotBeEmpty": "Alan adı boş olamaz", + "success": { + "deleteSpace": "Alan başarıyla silindi", + "renameSpace": "Alan başarıyla yeniden adlandırıldı", + "duplicateSpace": "Alan başarıyla çoğaltıldı", + "updateSpace": "Alan başarıyla güncellendi" + }, + "error": { + "deleteSpace": "Alan silinemedi", + "renameSpace": "Alan yeniden adlandırılamadı", + "duplicateSpace": "Alan çoğaltılamadı", + "updateSpace": "Alan güncellenemedi" + }, + "createSpace": "Alan oluştur", + "manageSpace": "Alanı yönet", + "renameSpace": "Alanı yeniden adlandır", + "mSpaceIconColor": "Alan ikonu rengi", + "mSpaceIcon": "Alan ikonu" }, "publish": { "hasNotBeenPublished": "Bu sayfa henüz yayınlanmadı", + "spaceHasNotBeenPublished": "Henüz bir alanın yayınlanması desteklenmiyor", "reportPage": "Sayfayı bildir", - "databaseHasNotBeenPublished": "Veritabanı yayınlama henüz desteklenmemektedir.", + "databaseHasNotBeenPublished": "Veritabanı yayınlama henüz desteklenmiyor.", "createdWith": "Şununla oluşturuldu", - "downloadApp": "AppFlowy'i İndir", + "downloadApp": "AppFlowy'yi İndir", "copy": { "codeBlock": "Kod bloğunun içeriği panoya kopyalandı", - "imageBlock": "Resim bağlantısı panoya kopyalandı", - "mathBlock": "Matematik denklemi panoya kopyalandı" + "imageBlock": "Görsel bağlantısı panoya kopyalandı", + "mathBlock": "Matematik denklemi panoya kopyalandı", + "fileBlock": "Dosya bağlantısı panoya kopyalandı" }, - "containsPublishedPage": "Bu sayfa bir veya daha fazla yayınlanmış sayfa içeriyor. Devam ederseniz, yayınlanmamış olacaklar. Silme işlemine devam etmek istiyor musunuz?", + "containsPublishedPage": "Bu sayfa bir veya daha fazla yayınlanmış sayfa içeriyor. Devam ederseniz, yayından kaldırılacaklar. Silme işlemine devam etmek istiyor musunuz?", "publishSuccessfully": "Başarıyla yayınlandı", - "unpublishSuccessfully": "Başarıyla yayınlanmamış", - "publishFailed": "Yayınlama başarısız oldu", - "unpublishFailed": "Yayından kaldırma başarısız oldu", + "unpublishSuccessfully": "Yayından kaldırma başarılı", + "publishFailed": "Yayınlanamadı", + "unpublishFailed": "Yayından kaldırılamadı", "noAccessToVisit": "Bu sayfaya erişim yok...", "createWithAppFlowy": "AppFlowy ile bir web sitesi oluşturun", - "fastWithAI": "Yapay Zeka ile hızlı ve kolay.", + "fastWithAI": "Yapay zeka ile hızlı ve kolay.", "tryItNow": "Şimdi deneyin", - "onlyGridViewCanBePublished": "Yalnızca Kılavuz görünümü yayınlanabilir", + "onlyGridViewCanBePublished": "Yalnızca Tablo görünümü yayınlanabilir", "database": { - "zero": "Seçilen {} görünümü yayınla", - "one": "Seçilen {} görünümü yayınla", - "many": "Seçilen {} görünümü yayınla", - "other": "Seçilen {} görünümü yayınla" + "zero": "{} seçili görünümü yayınla", + "one": "{} seçili görünümü yayınla", + "many": "{} seçili görünümü yayınla", + "other": "{} seçili görünümü yayınla" }, - "mustSelectPrimaryDatabase": "Birincil görünüm seçilmelidir", + "mustSelectPrimaryDatabase": "Ana görünüm seçilmelidir", "noDatabaseSelected": "Veritabanı seçilmedi, lütfen en az bir veritabanı seçin.", - "unableToDeselectPrimaryDatabase": "Birincil veritabanı seçimi kaldırılamıyor" + "unableToDeselectPrimaryDatabase": "Ana veritabanının seçimi kaldırılamaz", + "saveThisPage": "Bu şablonla başlayın", + "duplicateTitle": "Nereye eklemek istersiniz", + "selectWorkspace": "Bir çalışma alanı seçin", + "addTo": "Şuraya ekle", + "duplicateSuccessfully": "Çalışma alanınıza eklendi", + "duplicateSuccessfullyDescription": "AppFlowy yüklü değil mi? 'İndir'e tıkladıktan sonra indirme otomatik olarak başlayacak.", + "downloadIt": "İndir", + "openApp": "Uygulamada aç", + "duplicateFailed": "Çoğaltma başarısız", + "membersCount": { + "zero": "Üye yok", + "one": "1 üye", + "many": "{count} üye", + "other": "{count} üye" + }, + "useThisTemplate": "Bu şablonu kullan" }, "web": { - "continue": "Devam Et", + "continue": "Devam et", "or": "veya", - "continueWithGoogle": "Google ile Devam Et", - "continueWithGithub": "GitHub ile Devam Et", - "continueWithDiscord": "Discord ile Devam Et", - "signInAgreement": "Yukarıdaki \"Devam Et\" düğmesine tıklayarak,\nAppFlowy'nin", + "continueWithGoogle": "Google ile devam et", + "continueWithGithub": "GitHub ile devam et", + "continueWithDiscord": "Discord ile devam et", + "continueWithApple": "Apple ile devam et", + "moreOptions": "Daha fazla seçenek", + "collapse": "Daralt", + "signInAgreement": "Yukarıdaki \"Devam et\" düğmesine tıklayarak AppFlowy'nin şunlarını kabul etmiş olursunuz:", "and": "ve", "termOfUse": "Kullanım Koşulları", "privacyPolicy": "Gizlilik Politikası", - "signInError": "Oturum açma hatası", - "login": "Kaydolun veya giriş yapın" + "signInError": "Giriş hatası", + "login": "Kaydol veya giriş yap", + "fileBlock": { + "uploadedAt": "{time} tarihinde yüklendi", + "linkedAt": "Bağlantısı {time} tarihinde eklendi", + "empty": "Bir dosya yükleyin veya yerleştirin", + "uploadFailed": "Yükleme başarısız, lütfen tekrar deneyin", + "retry": "Tekrar dene" + }, + "importNotion": "Notion'dan içe aktar", + "import": "İçe aktar", + "importSuccess": "Başarıyla yüklendi", + "importSuccessMessage": "İçe aktarma tamamlandığında size bildirim göndereceğiz. Bundan sonra, içe aktarılan sayfalarınızı kenar çubuğunda görüntüleyebilirsiniz.", + "importFailed": "İçe aktarma başarısız, lütfen dosya formatını kontrol edin", + "dropNotionFile": "Yüklemek için Notion zip dosyanızı buraya bırakın veya göz atmak için tıklayın", + "error": { + "pageNameIsEmpty": "Sayfa adı boş, lütfen başka bir tane deneyin" + } + }, + "globalComment": { + "comments": "Yorumlar", + "addComment": "Yorum ekle", + "reactedBy": "tepki verenler", + "addReaction": "Tepki ekle", + "reactedByMore": "ve {count} diğer", + "showSeconds": { + "one": "1 saniye önce", + "other": "{count} saniye önce", + "zero": "Az önce", + "many": "{count} saniye önce" + }, + "showMinutes": { + "one": "1 dakika önce", + "other": "{count} dakika önce", + "many": "{count} dakika önce" + }, + "showHours": { + "one": "1 saat önce", + "other": "{count} saat önce", + "many": "{count} saat önce" + }, + "showDays": { + "one": "1 gün önce", + "other": "{count} gün önce", + "many": "{count} gün önce" + }, + "showMonths": { + "one": "1 ay önce", + "other": "{count} ay önce", + "many": "{count} ay önce" + }, + "showYears": { + "one": "1 yıl önce", + "other": "{count} yıl önce", + "many": "{count} yıl önce" + }, + "reply": "Yanıtla", + "deleteComment": "Yorumu sil", + "youAreNotOwner": "Bu yorumun sahibi siz değilsiniz", + "confirmDeleteDescription": "Bu yorumu silmek istediğinizden emin misiniz?", + "hasBeenDeleted": "Silindi", + "replyingTo": "Yanıtlanıyor", + "noAccessDeleteComment": "Bu yorumu silme izniniz yok", + "collapse": "Daralt", + "readMore": "Devamını oku", + "failedToAddComment": "Yorum eklenemedi", + "commentAddedSuccessfully": "Yorum başarıyla eklendi.", + "commentAddedSuccessTip": "Az önce bir yorum eklediniz veya yanıtladınız. En son yorumları görmek için başa dönmek ister misiniz?" + }, + "template": { + "asTemplate": "Şablon olarak kaydet", + "name": "Şablon adı", + "description": "Şablon Açıklaması", + "about": "Şablon Hakkında", + "deleteFromTemplate": "Şablonlardan sil", + "preview": "Şablon Önizleme", + "categories": "Şablon Kategorileri", + "isNewTemplate": "Yeni şablona SABİTLE", + "featured": "Öne Çıkanlara SABİTLE", + "relatedTemplates": "İlgili Şablonlar", + "requiredField": "{field} gereklidir", + "addCategory": "\"{category}\" ekle", + "addNewCategory": "Yeni kategori ekle", + "addNewCreator": "Yeni oluşturucu ekle", + "deleteCategory": "Kategoriyi sil", + "editCategory": "Kategoriyi düzenle", + "editCreator": "Oluşturucuyu düzenle", + "category": { + "name": "Kategori adı", + "icon": "Kategori ikonu", + "bgColor": "Kategori arka plan rengi", + "priority": "Kategori önceliği", + "desc": "Kategori açıklaması", + "type": "Kategori türü", + "icons": "Kategori İkonları", + "colors": "Kategori Renkleri", + "byUseCase": "Kullanım Durumuna Göre", + "byFeature": "Özelliğe Göre", + "deleteCategory": "Kategoriyi sil", + "deleteCategoryDescription": "Bu kategoriyi silmek istediğinizden emin misiniz?", + "typeToSearch": "Kategorilerde arama yapmak için yazın..." + }, + "creator": { + "label": "Şablon Oluşturucu", + "name": "Oluşturucu adı", + "avatar": "Oluşturucu avatarı", + "accountLinks": "Oluşturucu hesap bağlantıları", + "uploadAvatar": "Avatar yüklemek için tıklayın", + "deleteCreator": "Oluşturucuyu sil", + "deleteCreatorDescription": "Bu oluşturucuyu silmek istediğinizden emin misiniz?", + "typeToSearch": "Oluşturucularda arama yapmak için yazın..." + }, + "uploadSuccess": "Şablon başarıyla yüklendi", + "uploadSuccessDescription": "Şablonunuz başarıyla yüklendi. Artık şablon galerisinde görüntüleyebilirsiniz.", + "viewTemplate": "Şablonu görüntüle", + "deleteTemplate": "Şablonu sil", + "deleteSuccess": "Şablon başarıyla silindi", + "deleteTemplateDescription": "Bu işlem mevcut sayfayı veya yayınlanma durumunu etkilemeyecektir. Bu şablonu silmek istediğinizden emin misiniz?", + "addRelatedTemplate": "İlgili şablon ekle", + "removeRelatedTemplate": "İlgili şablonu kaldır", + "uploadAvatar": "Avatar yükle", + "searchInCategory": "{category} içinde ara", + "label": "Şablonlar" + }, + "fileDropzone": { + "dropFile": "Yüklemek için dosyayı bu alana tıklayın veya sürükleyin", + "uploading": "Yükleniyor...", + "uploadFailed": "Yükleme başarısız", + "uploadSuccess": "Yükleme başarılı", + "uploadSuccessDescription": "Dosya başarıyla yüklendi", + "uploadFailedDescription": "Dosya yüklenemedi", + "uploadingDescription": "Dosya yükleniyor" + }, + "gallery": { + "preview": "Tam ekran aç", + "copy": "Kopyala", + "download": "İndir", + "prev": "Önceki", + "next": "Sonraki", + "resetZoom": "Yakınlaştırmayı sıfırla", + "zoomIn": "Yakınlaştır", + "zoomOut": "Uzaklaştır" + }, + "invitation": { + "join": "Katıl", + "on": "tarihinde", + "invitedBy": "Davet eden", + "membersCount": { + "zero": "{count} üye", + "one": "{count} üye", + "many": "{count} üye", + "other": "{count} üye" + }, + "tip": "Aşağıdaki iletişim bilgileriyle bu çalışma alanına katılmaya davet edildiniz. Bu bilgiler yanlışsa, davetiyeyi yeniden göndermesi için yöneticinizle iletişime geçin.", + "joinWorkspace": "Çalışma alanına katıl", + "success": "Çalışma alanına başarıyla katıldınız", + "successMessage": "Artık içindeki tüm sayfalara ve çalışma alanlarına erişebilirsiniz.", + "openWorkspace": "AppFlowy'yi Aç", + "alreadyAccepted": "Bu daveti zaten kabul ettiniz", + "errorModal": { + "title": "Bir hata oluştu", + "description": "Mevcut hesabınızın {email} bu çalışma alanına erişimi olmayabilir. Lütfen doğru hesapla giriş yapın veya yardım için çalışma alanı sahibiyle iletişime geçin.", + "contactOwner": "Sahiple iletişime geç", + "close": "Ana sayfaya dön", + "changeAccount": "Hesap değiştir" + } + }, + "requestAccess": { + "title": "Bu sayfaya erişim yok", + "subtitle": "Bu sayfanın sahibinden erişim talep edebilirsiniz. Onaylandığında, sayfayı görüntüleyebilirsiniz.", + "requestAccess": "Erişim talep et", + "backToHome": "Ana sayfaya dön", + "tip": "Şu anda olarak giriş yapmış durumdasınız.", + "mightBe": "Farklı bir hesapla yapmanız gerekebilir.", + "successful": "Talep başarıyla gönderildi", + "successfulMessage": "Sahibi talebinizi onayladığında size bildirim gönderilecek.", + "requestError": "Erişim talebi başarısız oldu", + "repeatRequestError": "Bu sayfa için zaten erişim talebinde bulundunuz" + }, + "approveAccess": { + "title": "Çalışma Alanı Katılım Talebini Onayla", + "requestSummary": ", 'a katılmak ve 'a erişmek istiyor", + "upgrade": "yükselt", + "downloadApp": "AppFlowy'yi İndir", + "approveButton": "Onayla", + "approveSuccess": "Başarıyla onaylandı", + "approveError": "Onaylama başarısız, çalışma alanı plan limitinin aşılmadığından emin olun", + "getRequestInfoError": "Talep bilgisi alınamadı", + "memberCount": { + "zero": "Üye yok", + "one": "1 üye", + "many": "{count} üye", + "other": "{count} üye" + }, + "alreadyProTitle": "Çalışma alanı plan limitine ulaştınız", + "alreadyProMessage": "Daha fazla üye eklemek için ile iletişime geçmelerini isteyin", + "repeatApproveError": "Bu talebi zaten onayladınız", + "ensurePlanLimit": "Çalışma alanı plan limitinin aşılmadığından emin olun. Limit aşılırsa, çalışma alanı planını veya .", + "requestToJoin": "katılmak için talep etti", + "asMember": "üye olarak" + }, + "upgradePlanModal": { + "title": "Pro'ya Yükselt", + "message": "{name} ücretsiz üye limitine ulaştı. Daha fazla üye davet etmek için Pro Plana yükseltin.", + "upgradeSteps": "AppFlowy'de planınızı nasıl yükseltirsiniz:", + "step1": "1. Ayarlar'a gidin", + "step2": "2. 'Plan'a tıklayın", + "step3": "3. 'Planı Değiştir'i seçin", + "appNote": "Not: ", + "actionButton": "Yükselt", + "downloadLink": "Uygulamayı İndir", + "laterButton": "Sonra", + "refreshNote": "Başarılı yükseltmeden sonra, yeni özelliklerinizi etkinleştirmek için tıklayın.", + "refresh": "buraya" + }, + "breadcrumbs": { + "label": "Gezinti menüsü" + }, + "time": { + "justNow": "Az önce", + "seconds": { + "one": "1 saniye", + "other": "{count} saniye" + }, + "minutes": { + "one": "1 dakika", + "other": "{count} dakika" + }, + "hours": { + "one": "1 saat", + "other": "{count} saat" + }, + "days": { + "one": "1 gün", + "other": "{count} gün" + }, + "weeks": { + "one": "1 hafta", + "other": "{count} hafta" + }, + "months": { + "one": "1 ay", + "other": "{count} ay" + }, + "years": { + "one": "1 yıl", + "other": "{count} yıl" + }, + "ago": "önce", + "yesterday": "Dün", + "today": "Bugün" + }, + "members": { + "zero": "Üye yok", + "one": "1 üye", + "many": "{count} üye", + "other": "{count} üye" + }, + "tabMenu": { + "close": "Kapat", + "closeDisabledHint": "Sabitlenmiş bir sekme kapatılamaz, lütfen önce sabitlemeyi kaldırın", + "closeOthers": "Diğer sekmeleri kapat", + "closeOthersHint": "Bu işlem, bu sekme dışındaki tüm sabitlenmemiş sekmeleri kapatacaktır", + "closeOthersDisabledHint": "Tüm sekmeler sabitlenmiş, kapatılacak sekme bulunamadı", + "favorite": "Favorilere ekle", + "unfavorite": "Favorilerden kaldır", + "favoriteDisabledHint": "Bu görünüm favorilere eklenemez", + "pinTab": "Sabitle", + "unpinTab": "Sabitlemeyi kaldır" + }, + "openFileMessage": { + "success": "Dosya başarıyla açıldı", + "fileNotFound": "Dosya bulunamadı", + "noAppToOpenFile": "Bu dosyayı açacak uygulama yok", + "permissionDenied": "Bu dosyayı açma izni yok", + "unknownError": "Dosya açılamadı" + }, + "inviteMember": { + "requestInviteMembers": "Çalışma alanınıza davet et", + "inviteFailedMemberLimit": "Üye limitine ulaşıldı, lütfen ", + "upgrade": "yükselt", + "addEmail": "email@example.com, email2@example.com...", + "requestInvites": "Davetleri gönder", + "inviteAlready": "Bu e-postayı zaten davet ettiniz: {email}", + "inviteSuccess": "Davet başarıyla gönderildi", + "description": "E-postaları aralarına virgül koyarak aşağıya girin. Ücretlendirme üye sayısına göre yapılır.", + "emails": "E-posta" + }, + "quickNote": { + "label": "Hızlı Not", + "quickNotes": "Hızlı Notlar", + "search": "Hızlı Notlarda Ara", + "collapseFullView": "Tam görünümü daralt", + "expandFullView": "Tam görünümü genişlet", + "createFailed": "Hızlı Not oluşturulamadı", + "quickNotesEmpty": "Hızlı Not yok", + "emptyNote": "Boş not", + "deleteNotePrompt": "Seçilen not kalıcı olarak silinecektir. Silmek istediğinizden emin misiniz?", + "addNote": "Yeni Not", + "noAdditionalText": "Ek metin yok" + }, + "subscribe": { + "upgradePlanTitle": "Planları karşılaştır ve seç", + "yearly": "Yıllık", + "save": "%{discount} tasarruf", + "monthly": "Aylık", + "priceIn": "Fiyat: ", + "free": "Ücretsiz", + "pro": "Pro", + "freeDescription": "2 üyeye kadar bireyler için her şeyi organize etmek için", + "proDescription": "Küçük ekipler için projeleri ve ekip bilgisini yönetmek için", + "proDuration": { + "monthly": "üye başına aylık\naylık faturalandırma", + "yearly": "üye başına aylık\nyıllık faturalandırma" + }, + "cancel": "Alt plana geç", + "changePlan": "Pro Plana yükselt", + "everythingInFree": "Ücretsiz plandaki her şey +", + "currentPlan": "Mevcut", + "freeDuration": "süresiz", + "freePoints": { + "first": "2 üyeye kadar 1 işbirliği çalışma alanı", + "second": "Sınırsız sayfa ve blok", + "three": "5 GB depolama", + "four": "Akıllı arama", + "five": "20 yapay zeka yanıtı", + "six": "Mobil uygulama", + "seven": "Gerçek zamanlı işbirliği" + }, + "proPoints": { + "first": "Sınırsız depolama", + "second": "10 çalışma alanı üyesine kadar", + "three": "Sınırsız yapay zeka yanıtı", + "four": "Sınırsız dosya yükleme", + "five": "Özel alan adı" + }, + "cancelPlan": { + "title": "Gitmenize üzüldük", + "success": "Aboneliğiniz başarıyla iptal edildi", + "description": "Gitmenize üzüldük. AppFlowy'yi geliştirmemize yardımcı olmak için geri bildiriminizi almak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.", + "commonOther": "Diğer", + "otherHint": "Yanıtınızı buraya yazın", + "questionOne": { + "question": "AppFlowy Pro aboneliğinizi iptal etmenize ne sebep oldu?", + "answerOne": "Maliyet çok yüksek", + "answerTwo": "Özellikler beklentileri karşılamadı", + "answerThree": "Daha iyi bir alternatif buldum", + "answerFour": "Maliyeti karşılayacak kadar kullanmadım", + "answerFive": "Hizmet sorunu veya teknik zorluklar" + }, + "questionTwo": { + "question": "Gelecekte AppFlowy Pro'ya yeniden abone olma olasılığınız nedir?", + "answerOne": "Çok muhtemel", + "answerTwo": "Biraz muhtemel", + "answerThree": "Emin değilim", + "answerFour": "Muhtemel değil", + "answerFive": "Hiç muhtemel değil" + }, + "questionThree": { + "question": "Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?", + "answerOne": "Çoklu kullanıcı işbirliği", + "answerTwo": "Daha uzun süreli versiyon geçmişi", + "answerThree": "Sınırsız yapay zeka yanıtları", + "answerFour": "Yerel yapay zeka modellerine erişim" + }, + "questionFour": { + "question": "AppFlowy ile genel deneyiminizi nasıl tanımlarsınız?", + "answerOne": "Harika", + "answerTwo": "İyi", + "answerThree": "Ortalama", + "answerFour": "Ortalamanın altında", + "answerFive": "Memnun değilim" + } + } + }, + "ai": { + "contentPolicyViolation": "Hassas içerik nedeniyle görsel oluşturma başarısız oldu. Lütfen girdinizi yeniden düzenleyip tekrar deneyin" } } From 17ae05a623371f483c9cd227ecb4a9022df93abd Mon Sep 17 00:00:00 2001 From: Morn Date: Fri, 7 Feb 2025 14:49:50 +0800 Subject: [PATCH 016/384] fix: pasting a link on iOS results in incorrect behavior (#7326) --- .../lib/plugins/document/presentation/editor_page.dart | 1 + .../editor_plugins/copy_and_paste/clipboard_service.dart | 3 ++- .../copy_and_paste/custom_paste_command.dart | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index d83bd055ce..5c3762b438 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -111,6 +111,7 @@ class _AppFlowyEditorPageState extends State } EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; + DocumentBloc get documentBloc => context.read(); late final EditorScrollController editorScrollController; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart index f4aac4fe2c..f108c7e26b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart @@ -132,6 +132,7 @@ class ClipboardService { final html = await reader.readValue(Formats.htmlText); final inAppJson = await reader.readValue(inAppJsonFormat); final tableJson = await reader.readValue(tableJsonFormat); + final uri = await reader.readValue(Formats.uri); (String, Uint8List?)? image; if (reader.canProvide(Formats.png)) { image = ('png', await reader.readFile(Formats.png)); @@ -144,7 +145,7 @@ class ClipboardService { } return ClipboardServiceData( - plainText: plainText, + plainText: plainText ?? uri?.uri.toString(), html: html, image: image, inAppJson: inAppJson, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index c523453fba..5c25d470d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -144,6 +144,13 @@ Future doPaste(EditorState editorState) async { } if (plainText != null && plainText.isNotEmpty) { + final currentSelection = editorState.selection; + if (currentSelection == null) { + await editorState.updateSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ); + } await editorState.pasteText(plainText); return Log.info('Pasted plain text'); } From 00cdee831d6fbe6ec06411c122ec8547bfed0a43 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 7 Feb 2025 18:17:46 +0800 Subject: [PATCH 017/384] chore: upgrade to Flutter 3.27.4 (#7230) --- .github/workflows/android_ci.yaml.bak | 2 +- .github/workflows/flutter_ci.yaml | 22 +- .github/workflows/ios_ci.yaml | 2 +- .github/workflows/release.yml | 22 +- .github/workflows/rust_coverage.yml | 2 +- codemagic.yaml | 2 +- .../integration_test/shared/emoji.dart | 3 +- .../prompt_input/mention_page_menu.dart | 4 +- .../appflowy_flutter/lib/env/cloud_env.dart | 2 +- .../lib/flutter/af_dropdown_menu.dart | 4 +- .../base/view_page/app_bar_buttons.dart | 4 +- .../show_mobile_bottom_sheet.dart | 2 +- .../show_transition_bottom_sheet.dart | 2 +- .../database/board/mobile_board_page.dart | 13 +- .../field/mobile_edit_field_screen.dart | 5 +- .../view/database_view_quick_actions.dart | 6 +- .../home/mobile_home_trash_page.dart | 2 +- .../recent_folder/mobile_recent_view.dart | 3 +- .../home/shared/mobile_page_card.dart | 2 +- .../home/tab/ai_bubble_button.dart | 6 +- .../home/tab/mobile_space_tab.dart | 2 - .../workspace_menu_bottom_sheet.dart | 2 +- .../workspaces/workspace_more_options.dart | 2 - .../mobile_bottom_navigation_bar.dart | 6 +- .../notifications/widgets/color.dart | 2 +- .../setting/appearance/rtl_setting.dart | 1 - .../widgets/flowy_option_tile.dart | 2 +- .../show_flowy_mobile_confirm_dialog.dart | 2 +- .../presentation/animated_chat_list.dart | 3 +- .../presentation/chat_editor_style.dart | 5 +- .../presentation/chat_welcome_page.dart | 6 +- .../message/ai_message_action_bar.dart | 12 +- .../presentation/scroll_to_bottom.dart | 6 +- .../board/presentation/board_page.dart | 16 +- .../calendar/presentation/calendar_day.dart | 6 +- .../presentation/calendar_event_card.dart | 16 +- .../presentation/calendar_event_editor.dart | 16 +- .../database/grid/presentation/grid_page.dart | 12 +- .../widgets/footer/grid_footer.dart | 11 +- .../widgets/header/desktop_field_cell.dart | 11 +- .../widgets/card/container/accessory.dart | 11 +- .../desktop_row_detail_media_cell.dart | 7 +- .../database/widgets/row/row_banner.dart | 11 +- .../database_document_plugin.dart | 2 +- .../document/application/document_bloc.dart | 4 +- .../document_data_pb_extension.dart | 3 +- .../editor_transaction_adapter.dart | 2 +- .../lib/plugins/document/document.dart | 2 +- .../collaborator_avater_stack.dart | 2 +- .../document/presentation/editor_page.dart | 2 +- .../actions/mobile_block_action_buttons.dart | 1 - .../align_toolbar_item.dart | 4 +- .../editor_plugins/file/file_upload_menu.dart | 5 +- .../file/mobile_file_upload_menu.dart | 6 +- .../font/customize_font_toolbar_item.dart | 2 +- .../header/document_cover_widget.dart | 12 +- .../header/emoji_icon_widget.dart | 7 +- .../heading/heading_toolbar_item.dart | 4 +- .../image_menu.dart | 2 +- .../layouts/image_browser_layout.dart | 17 +- .../layouts/multi_image_layouts.dart | 4 +- .../multi_image_menu.dart | 7 +- .../image/resizeable_image.dart | 6 +- .../widgets/embed_image_url_widget.dart | 3 +- .../link_preview/link_preview_menu.dart | 2 +- .../editor_plugins/mention/mention_block.dart | 2 - .../aa_menu/_color_list.dart | 7 +- .../simple_table_cell_block_component.dart | 2 +- .../simple_table/simple_table_constants.dart | 2 +- .../simple_table_more_action.dart | 4 +- .../_simple_table_bottom_sheet_actions.dart | 2 +- .../simple_table_bottom_sheet.dart | 4 +- .../sub_page/block_transaction_handler.dart | 7 +- .../document/presentation/editor_style.dart | 11 +- .../widgets/inline_actions_handler.dart | 2 +- .../lib/plugins/shared/share/export_tab.dart | 2 +- .../shared/share/publish_color_extension.dart | 2 +- .../lib/plugins/shared/share/publish_tab.dart | 4 +- .../lib/shared/icon_emoji_picker/colors.dart | 6 +- .../popup_menu/appflowy_popup_menu.dart | 2 +- .../lib/startup/plugin/plugin.dart | 2 +- .../lib/util/color_to_hex_string.dart | 9 +- .../application/appearance_defaults.dart | 5 +- .../settings/appearance/appearance_cubit.dart | 3 - .../appearance/mobile_appearance.dart | 2 +- .../shortcuts/settings_shortcuts_service.dart | 4 +- .../widgets/recent_view_tile.dart | 4 +- .../widgets/search_result_tile.dart | 3 +- .../presentation/home/home_stack.dart | 2 +- .../menu/sidebar/footer/sidebar_toast.dart | 4 +- .../menu/sidebar/import/import_panel.dart | 2 - .../menu/sidebar/space/shared_widget.dart | 8 +- .../workspace/_sidebar_workspace_menu.dart | 4 +- .../home/menu/view/draggable_view_item.dart | 8 +- .../pages/account/account_deletion.dart | 3 +- .../settings/pages/settings_plan_view.dart | 6 +- .../pages/settings_shortcuts_view.dart | 2 +- .../pages/settings_workspace_view.dart | 8 +- .../pages/sites/domain/domain_item.dart | 2 +- .../settings/settings_dialog.dart | 2 - .../shared/flowy_gradient_button.dart | 5 +- .../settings/shared/settings_dropdown.dart | 2 +- .../shared/single_setting_action.dart | 4 +- .../emoji_picker/src/emji_picker_config.dart | 4 +- .../theme_upload/theme_upload_decoration.dart | 6 +- .../theme_upload_failure_widget.dart | 2 +- .../theme_upload_loading_widget.dart | 2 +- .../theme_upload/upload_new_theme_widget.dart | 2 +- .../float_bubble/social_media_section.dart | 3 - .../interactive_image_toolbar.dart | 9 +- frontend/appflowy_flutter/macos/Podfile.lock | 27 +- .../macos/Runner/AppDelegate.swift | 2 +- .../example/lib/example_button.dart | 2 +- .../lib/appflowy_popover.dart | 2 +- .../appflowy_result/lib/appflowy_result.dart | 3 +- .../lib/utils/color_converter.dart | 9 +- .../flowy_overlay/flowy_popover_layout.dart | 4 +- .../lib/src/flowy_overlay/layout.dart | 4 +- .../lib/src/flowy_overlay/list_overlay.dart | 2 +- .../style_widget/primary_rounded_button.dart | 4 +- .../lib/style_widget/text_input.dart | 5 +- .../widget/buttons/base_styled_button.dart | 2 +- .../lib/widget/dialog/styled_dialogs.dart | 2 +- .../lib/widget/flowy_tooltip.dart | 2 +- .../packages/flowy_svg/lib/src/flowy_svg.dart | 2 +- frontend/appflowy_flutter/pubspec.lock | 420 +++++++++++------- frontend/appflowy_flutter/pubspec.yaml | 36 +- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- .../scripts/install_dev_env/install_ios.sh | 18 +- .../scripts/install_dev_env/install_linux.sh | 12 +- .../scripts/install_dev_env/install_macos.sh | 12 +- .../install_dev_env/install_windows.sh | 12 +- 132 files changed, 636 insertions(+), 506 deletions(-) diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak index 4e159e845b..81e132cbf8 100644 --- a/.github/workflows/android_ci.yaml.bak +++ b/.github/workflows/android_ci.yaml.bak @@ -18,7 +18,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.22.3" + FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" CARGO_MAKE_VERSION: "0.37.18" CLOUD_VERSION: 0.6.54-amd64 diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 42daeca881..1fc1b0e052 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -25,7 +25,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.22.2" + FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" CARGO_MAKE_VERSION: "0.37.18" CLOUD_VERSION: 0.6.54-amd64 @@ -40,7 +40,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ windows-latest ] + os: [windows-latest] include: - os: windows-latest flutter_profile: development-windows-x86 @@ -101,7 +101,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ macos-latest ] + os: [macos-latest] include: - os: macos-latest flutter_profile: development-mac-x86_64 @@ -123,12 +123,12 @@ jobs: flutter_profile: ${{ matrix.flutter_profile }} unit_test: - needs: [ prepare-linux ] + needs: [prepare-linux] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -217,11 +217,11 @@ jobs: shell: bash cloud_integration_test: - needs: [ prepare-linux ] + needs: [prepare-linux] strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -340,13 +340,13 @@ jobs: shell: bash integration_test: - needs: [ prepare-linux ] + needs: [prepare-linux] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] - test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] + os: [ubuntu-latest] + test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9] include: - os: ubuntu-latest target: "x86_64-unknown-linux-gnu" diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index d9bee0242a..e13863f4a7 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -18,7 +18,7 @@ on: - "!frontend/appflowy_web_app/**" env: - FLUTTER_VERSION: "3.22.3" + FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91e6b99565..918a2018f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*" env: - FLUTTER_VERSION: "3.22.0" + FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" jobs: @@ -232,10 +232,10 @@ jobs: matrix: job: - { - targets: "aarch64-apple-darwin,x86_64-apple-darwin", - os: macos-latest, - extra-build-args: "", - } + targets: "aarch64-apple-darwin,x86_64-apple-darwin", + os: macos-latest, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v4 @@ -336,12 +336,12 @@ jobs: matrix: job: - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-args: "", - flutter_profile: production-linux-x86_64, - } + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-args: "", + flutter_profile: production-linux-x86_64, + } steps: - name: Checkout source code uses: actions/checkout@v4 diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 916f19d9fc..53a5f66748 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -10,7 +10,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.22.0" + FLUTTER_VERSION: "3.27.4" RUST_TOOLCHAIN: "1.81.0" jobs: diff --git a/codemagic.yaml b/codemagic.yaml index b8934d8be8..9ba2a1a562 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -4,7 +4,7 @@ workflows: instance_type: mac_mini_m2 max_build_duration: 30 environment: - flutter: 3.22.3 + flutter: 3.27.4 xcode: latest cocoapods: default diff --git a/frontend/appflowy_flutter/integration_test/shared/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart index 0f45098b23..41301dd9c1 100644 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/shared/emoji.dart @@ -6,7 +6,6 @@ import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; import 'package:flowy_svg/flowy_svg.dart'; @@ -90,7 +89,7 @@ extension EmojiTestExtension on WidgetTester { final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget; dropTargetWidget.onDragDone?.call( DropDoneDetails( - files: [XFile(icon.emoji)], + files: [DropItemFile(icon.emoji)], localPosition: Offset.zero, globalPosition: Offset.zero, ), diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart index 69fecad613..ae2dbe5f26 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart @@ -49,7 +49,9 @@ class _PromptInputMentionPageMenuState void initState() { super.initState(); Future.delayed(Duration.zero, () { - context.read().refreshViews(); + if (mounted) { + context.read().refreshViews(); + } }); } diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 3df88eac24..986fab128b 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -180,7 +180,7 @@ Future useLocalServer() async { await _setAuthenticatorType(AuthenticatorType.local); } -/// Use getIt() to get the shared environment. +// Use getIt() to get the shared environment. class AppFlowyCloudSharedEnv { AppFlowyCloudSharedEnv({ required AuthenticatorType authenticatorType, diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index 1cc846339b..56a61e120b 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -502,11 +502,11 @@ class _AFDropdownMenuState extends State> { // Simulate the focused state because the text field should always be focused // during traversal. If the menu item has a custom foreground color, the "focused" - // color will also change to foregroundColor.withOpacity(0.12). + // color will also change to foregroundColor.withValues(alpha: 0.12). effectiveStyle = entry.enabled && i == focusedIndex ? effectiveStyle.copyWith( backgroundColor: WidgetStatePropertyAll( - focusedBackgroundColor.withOpacity(0.12), + focusedBackgroundColor.withValues(alpha: 0.12), ), ) : effectiveStyle; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index b01f72c371..2d52426bf9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -43,7 +43,7 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget valueListenable: appBarOpacity, builder: (_, opacity, __) => FlowyAppBar( backgroundColor: - AppBarTheme.of(context).backgroundColor?.withOpacity(opacity), + AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity), showDivider: false, title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title), leadingWidth: 44, @@ -224,7 +224,7 @@ class _ImmersiveAppBarButton extends StatelessWidget { child = DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(dimension / 2.0), - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), ), child: child, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index 999813d63e..a0fa5dc6aa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -74,7 +74,7 @@ Future showMobileBottomSheet( backgroundColor ??= Theme.of(context).brightness == Brightness.light ? const Color(0xFFF7F8FB) : const Color(0xFF23262B); - barrierColor ??= Colors.black.withOpacity(0.3); + barrierColor ??= Colors.black.withValues(alpha: 0.3); return showModalBottomSheet( context: context, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart index 0ff2a6634a..b29817251a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart @@ -329,7 +329,7 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget { (Theme.of(context).brightness == Brightness.dark ? Colors.grey : Colors.black) - .withOpacity(secondaryAnimation.value * 0.1), + .withValues(alpha: secondaryAnimation.value * 0.1), BlendMode.srcOver, ), child: child, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart index 2885f37bbd..2cfd807174 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/board/board.dart'; @@ -19,6 +17,7 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -276,14 +275,20 @@ class _BoardContentState extends State<_BoardContent> { border: themeMode == ThemeMode.light ? Border.fromBorderSide( BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), ), ) : null, boxShadow: themeMode == ThemeMode.light ? [ BoxShadow( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), blurRadius: 4, offset: const Offset(0, 2), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart index a51e3561f8..75b52de414 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart'; @@ -8,6 +6,7 @@ import 'package:appflowy/plugins/database/domain/field_backend_service.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class MobileEditPropertyScreen extends StatefulWidget { @@ -49,7 +48,7 @@ class _MobileEditPropertyScreenState extends State { final fieldId = widget.field.id; return PopScope( - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (!didPop) { context.pop(_fieldOptionValues); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart index f665455dbe..505e012f7b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -84,7 +84,11 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { ); }, builder: (_) => const SizedBox.shrink(), - ).then((_) => Navigator.pop(context)); + ).then((_) { + if (context.mounted) { + Navigator.pop(context); + } + }); }, !isInline, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index 73a5381d42..73da7594a7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -212,7 +212,7 @@ class _DeletedFilesListView extends StatelessWidget { ?.copyWith(color: theme.colorScheme.onSurface), ), horizontalTitleGap: 0, - tileColor: theme.colorScheme.onSurface.withOpacity(0.1), + tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart index cad05acad4..966b1ac61a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -134,7 +134,8 @@ class _RecentCover extends StatelessWidget { Widget build(BuildContext context) { final placeholder = Container( // random color, update it once we have a better placeholder - color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2), + color: + Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.2), ); final value = this.value; if (value == null) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart index 638e20839e..87ce41d5b6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart @@ -225,7 +225,7 @@ class MobileViewPage extends StatelessWidget { Widget _buildLastViewed(BuildContext context) { final textColor = Theme.of(context).isLightMode ? const Color(0x7F171717) - : Colors.white.withOpacity(0.45); + : Colors.white.withValues(alpha: 0.45); if (timestamp == null) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart index b3f10bbbd2..cc4176e0ef 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart @@ -42,7 +42,7 @@ class FloatingAIEntry extends StatelessWidget { blurRadius: 20, spreadRadius: 1, offset: const Offset(0, 4), - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), ), ], ); @@ -51,8 +51,8 @@ class FloatingAIEntry extends StatelessWidget { BoxDecoration _buildWrapperDecoration(BuildContext context) { final outlineColor = Theme.of(context).colorScheme.outline; final borderColor = Theme.of(context).isLightMode - ? outlineColor.withOpacity(0.7) - : outlineColor.withOpacity(0.3); + ? outlineColor.withValues(alpha: 0.7) + : outlineColor.withValues(alpha: 0.3); return BoxDecoration( borderRadius: BorderRadius.circular(30), color: Theme.of(context).colorScheme.surface, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 1a19845152..cba6f0fd9a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -173,8 +173,6 @@ class _MobileSpaceTabState extends State ); case MobileSpaceTabType.favorites: return MobileFavoriteSpace(userProfile: widget.userProfile); - default: - throw Exception('Unknown tab type: $tab'); } }).toList(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index 9a5bd8c511..ef7f4492a5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -139,7 +139,7 @@ class _CreateWorkspaceButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( - color: const Color(0x01717171).withOpacity(0.12), + color: const Color(0x01717171).withValues(alpha: 0.12), width: 0.8, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart index 5f6066930f..bb6f6207f6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart @@ -101,8 +101,6 @@ class WorkspaceMenuMoreOptions extends StatelessWidget { WorkspaceMenuMoreOption.leave, ), ); - default: - return const Placeholder(); } } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 4d8bc16103..170ef46ac2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -365,14 +365,14 @@ class _NotificationNavigationBar extends StatelessWidget { extension on BuildContext { Color get backgroundColor { return Theme.of(this).isLightMode - ? Colors.white.withOpacity(0.95) - : const Color(0xFF23262B).withOpacity(0.95); + ? Colors.white.withValues(alpha: 0.95) + : const Color(0xFF23262B).withValues(alpha: 0.95); } Color get borderColor { return Theme.of(this).isLightMode ? const Color(0x141F2329) - : const Color(0xFF23262B).withOpacity(0.5); + : const Color(0xFF23262B).withValues(alpha: 0.5); } Border? get border { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart index 8a59336378..e11e91ada5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart @@ -6,6 +6,6 @@ extension NotificationItemColors on BuildContext { if (Theme.of(this).isLightMode) { return const Color(0xFF171717); } - return const Color(0xFFffffff).withOpacity(0.8); + return const Color(0xFFffffff).withValues(alpha: 0.8); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart index 8e1aceefae..5b8035f004 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart @@ -83,7 +83,6 @@ class RTLSetting extends StatelessWidget { case AppFlowyTextDirection.rtl: return LocaleKeys.settings_appearance_textDirection_rtl.tr(); case AppFlowyTextDirection.ltr: - default: return LocaleKeys.settings_appearance_textDirection_ltr.tr(); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index 4f76003e23..835b3b052f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -299,7 +299,7 @@ class _Toggle extends StatelessWidget { fit: BoxFit.fill, child: CupertinoSwitch( value: value, - activeColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, onChanged: onChanged, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index 9df9d2e6fd..96c18f5d91 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -99,7 +99,7 @@ Future showFlowyCupertinoConfirmDialog({ }) { return showDialog( context: context ?? AppGlobals.context, - barrierColor: Colors.black.withOpacity(0.25), + barrierColor: Colors.black.withValues(alpha: 0.25), builder: (context) => CupertinoAlertDialog( title: FlowyText.medium( title, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart index e4fc090605..9b7aadf4a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart @@ -7,10 +7,9 @@ import 'package:diffutil_dart/diffutil.dart' as diffutil; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; -import 'package:provider/provider.dart'; - import 'package:flutter_chat_ui/src/scroll_to_bottom.dart'; import 'package:flutter_chat_ui/src/utils/message_list_diff.dart'; +import 'package:provider/provider.dart'; class ChatAnimatedListReversed extends StatefulWidget { const ChatAnimatedListReversed({ diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart index a979e80746..b79e3c52c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart @@ -75,7 +75,8 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer { fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), + backgroundColor: + theme.colorScheme.inverseSurface.withValues(alpha: 0.8), ), ), ), @@ -144,7 +145,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer { return TextStyle( fontFamily: defaultFontFamily, height: 1.5, - color: AFThemeExtension.of(context).onBackground.withOpacity(0.6), + color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index 6c3dd4bd4d..d7a90bd18a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -125,14 +125,14 @@ class WelcomeSampleQuestion extends StatelessWidget { spreadRadius: -2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), blurRadius: 4, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), @@ -140,7 +140,7 @@ class WelcomeSampleQuestion extends StatelessWidget { spreadRadius: 2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 7ae4a11f19..1dbeeb0ec2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -89,14 +89,14 @@ class _AIMessageActionBarState extends State { spreadRadius: -2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), blurRadius: 4, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), @@ -104,7 +104,7 @@ class _AIMessageActionBarState extends State { spreadRadius: 2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), ], ), @@ -340,14 +340,14 @@ class _ChangeFormatPopoverContentState spreadRadius: -2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), blurRadius: 4, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), BoxShadow( offset: const Offset(0, 2), @@ -355,7 +355,7 @@ class _ChangeFormatPopoverContentState spreadRadius: 2, color: isLightMode ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withOpacity(0.02), + : Theme.of(context).shadowColor.withValues(alpha: 0.02), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart index 0cfa2efe7b..d66a6665b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart @@ -41,21 +41,21 @@ class CustomScrollToBottom extends StatelessWidget { spreadRadius: 8, color: isLightMode ? const Color(0x0F1F2329) - : Theme.of(context).shadowColor.withOpacity(0.06), + : Theme.of(context).shadowColor.withValues(alpha: 0.06), ), BoxShadow( offset: const Offset(0, 4), blurRadius: 8, color: isLightMode ? const Color(0x141F2329) - : Theme.of(context).shadowColor.withOpacity(0.08), + : Theme.of(context).shadowColor.withValues(alpha: 0.08), ), BoxShadow( offset: const Offset(0, 2), blurRadius: 4, color: isLightMode ? const Color(0x1F1F2329) - : Theme.of(context).shadowColor.withOpacity(0.12), + : Theme.of(context).shadowColor.withValues(alpha: 0.12), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index cefcafa03b..b170ee2fef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -1,10 +1,5 @@ import 'dart:io'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:flutter/material.dart' hide Card; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart'; @@ -20,6 +15,8 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desk import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/shared/conditional_listenable_builder.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; +import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -27,13 +24,14 @@ 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/style_widget/hover.dart'; +import 'package:flutter/material.dart' hide Card; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; import '../../widgets/card/card.dart'; import '../../widgets/cell/card_cell_builder.dart'; import '../application/board_bloc.dart'; - import 'toolbar/board_setting_bar.dart'; import 'widgets/board_focus_scope.dart'; import 'widgets/board_hidden_groups.dart'; @@ -715,19 +713,19 @@ class _BoardCardState extends State<_BoardCard> { .isFocused(GroupedRowId(rowId: rowId, groupId: groupId)) ? Theme.of(context).colorScheme.primary : Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withOpacity(0.12) + ? const Color(0xFF1F2329).withValues(alpha: 0.12) : const Color(0xFF59647A), ), ), boxShadow: [ BoxShadow( blurRadius: 4, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), ), BoxShadow( blurRadius: 4, spreadRadius: -2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart index de5648291e..1d2838210d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart @@ -256,16 +256,16 @@ class NewEventButton extends StatelessWidget { boxShadow: [ BoxShadow( spreadRadius: -2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 2, ), BoxShadow( - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 4, ), BoxShadow( spreadRadius: 2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 8, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 8df96350f9..5ef2e2c327 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -1,25 +1,23 @@ -import 'package:appflowy/plugins/database/application/row/row_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart'; +import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:universal_platform/universal_platform.dart'; import '../application/calendar_bloc.dart'; - import 'calendar_event_editor.dart'; class EventCard extends StatefulWidget { @@ -129,16 +127,16 @@ class _EventCardState extends State { boxShadow: [ BoxShadow( spreadRadius: -2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 2, ), BoxShadow( - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 4, ), BoxShadow( spreadRadius: 2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), blurRadius: 8, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index 576e5d198c..6c4b365906 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -98,12 +98,15 @@ class EventEditorControls extends StatelessWidget { size: const Size.square(16), color: Theme.of(context).iconTheme.color, ), - onPressed: () => context.read().add( - CalendarEvent.duplicateEvent( - rowController.viewId, - rowController.rowId, - ), - ), + onPressed: () { + context.read().add( + CalendarEvent.duplicateEvent( + rowController.viewId, + rowController.rowId, + ), + ); + PopoverContainer.of(context).close(); + }, ), ), const HSpace(8.0), @@ -126,6 +129,7 @@ class EventEditorControls extends StatelessWidget { rowController.rowId, ), ); + PopoverContainer.of(context).close(); }, ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index fb32a2868b..851ed2f7b1 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -1,19 +1,17 @@ import 'dart:async'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; -import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/domain/sort_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; +import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -21,6 +19,7 @@ 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/style_widget/scrolling/styled_scrollview.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:provider/provider.dart'; @@ -30,7 +29,6 @@ import '../../application/row/row_controller.dart'; import '../../tab_bar/tab_bar_view.dart'; import '../../widgets/row/row_detail.dart'; import '../application/grid_bloc.dart'; - import 'grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; @@ -474,7 +472,7 @@ class _GridRowsState extends State<_GridRows> { proxyDecorator: (child, _, __) => Provider.value( value: context.read(), child: Material( - color: Colors.white.withOpacity(.1), + color: Colors.white.withValues(alpha: .1), child: Opacity(opacity: .5, child: child), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart index 1b973554cd..43a0301a10 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; @@ -9,6 +7,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GridAddRowButton extends StatelessWidget { @@ -17,8 +16,8 @@ class GridAddRowButton extends StatelessWidget { @override Widget build(BuildContext context) { final color = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF171717).withOpacity(0.4) - : const Color(0xFFFFFFFF).withOpacity(0.4); + ? const Color(0xFF171717).withValues(alpha: 0.4) + : const Color(0xFFFFFFFF).withValues(alpha: 0.4); return FlowyButton( radius: BorderRadius.zero, decoration: BoxDecoration( @@ -66,8 +65,8 @@ class GridRowLoadMoreButton extends StatelessWidget { final padding = context.read().horizontalPadding; final color = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF171717).withOpacity(0.4) - : const Color(0xFFFFFFFF).withOpacity(0.4); + ? const Color(0xFF171717).withValues(alpha: 0.4) + : const Color(0xFFFFFFFF).withValues(alpha: 0.4); return Container( padding: GridSize.footerContentInsets.copyWith(left: 0) + diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index f0597c15e4..23c2fe1f91 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -1,17 +1,16 @@ -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; @@ -261,7 +260,7 @@ class FieldIcon extends StatelessWidget { return svgContent == null ? FlowySvg( fieldInfo.fieldType.svgData, - color: color.withOpacity(0.6), + color: color.withValues(alpha: 0.6), size: Size.square(dimension), ) : SizedBox.square( @@ -269,7 +268,7 @@ class FieldIcon extends StatelessWidget { child: Center( child: FlowySvg.string( svgContent, - color: color.withOpacity(0.45), + color: color.withValues(alpha: 0.45), size: Size.square(dimension - 2), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart index 7078685845..e74f947b46 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; enum AccessoryType { edit, @@ -45,7 +44,7 @@ class CardAccessoryContainer extends StatelessWidget { width: 1, thickness: 1, color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withOpacity(0.12) + ? const Color(0xFF1F2329).withValues(alpha: 0.12) : const Color(0xff59647a), ), ); @@ -77,19 +76,19 @@ class CardAccessoryContainer extends StatelessWidget { border: Border.fromBorderSide( BorderSide( color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withOpacity(0.12) + ? const Color(0xFF1F2329).withValues(alpha: 0.12) : const Color(0xff59647a), ), ), boxShadow: [ BoxShadow( blurRadius: 4, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), ), BoxShadow( blurRadius: 4, spreadRadius: -2, - color: const Color(0xFF1F2329).withOpacity(0.02), + color: const Color(0xFF1F2329).withValues(alpha: 0.02), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart index 34d8a18ad8..6e648eb187 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart @@ -202,7 +202,7 @@ class _FilePreviewFeedback extends StatelessWidget { decoration: BoxDecoration( boxShadow: [ BoxShadow( - color: const Color(0xFF1F2329).withOpacity(.2), + color: const Color(0xFF1F2329).withValues(alpha: .2), blurRadius: 6, offset: const Offset(0, 3), ), @@ -431,7 +431,8 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> { Positioned.fill( child: DecoratedBox( position: DecorationPosition.foreground, - decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)), + decoration: + BoxDecoration(color: Colors.black.withValues(alpha: 0.5)), child: child, ), ), @@ -543,7 +544,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> { setState(() => isSelected = true); controller.show(); }, - fillColor: Colors.black.withOpacity(0.4), + fillColor: Colors.black.withValues(alpha: 0.4), width: 18, radius: BorderRadius.circular(4), icon: const FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 243eb5f027..5b77e93f8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; @@ -31,6 +28,8 @@ 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/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:string_validator/string_validator.dart'; @@ -278,8 +277,10 @@ class _RowCoverState extends State { onPressed: () => popoverController.show(), hoverColor: Theme.of(context).colorScheme.surface, textColor: Theme.of(context).colorScheme.tertiary, - fillColor: - Theme.of(context).colorScheme.surface.withOpacity(0.5), + fillColor: Theme.of(context) + .colorScheme + .surface + .withValues(alpha: 0.5), title: LocaleKeys.document_plugins_cover_changeCover.tr(), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart index 07c2a4b5dc..fd238271b7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart @@ -1,4 +1,4 @@ -library document_plugin; +library; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index f7ec313471..641344cf27 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -399,7 +399,7 @@ class DocumentBloc extends Bloc { final basicColor = ColorGenerator(id.toString()).toColor(); final metadata = DocumentAwarenessMetadata( cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withOpacity(0.6).toHexString(), + selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), userName: user.name, userAvatar: user.iconUrl, ); @@ -422,7 +422,7 @@ class DocumentBloc extends Bloc { final basicColor = ColorGenerator(id.toString()).toColor(); final metadata = DocumentAwarenessMetadata( cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withOpacity(0.6).toHexString(), + selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), userName: user.name, userAvatar: user.iconUrl, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index f9a80864f1..574ae34af8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -18,7 +18,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' BulletedListBlockKeys, blockComponentDelta; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; class ExternalValues extends NodeExternalValues { @@ -105,7 +104,7 @@ extension DocumentDataPBFromTo on DocumentDataPB { final children = []; if (childrenIds != null && childrenIds.isNotEmpty) { - children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull()); + children.addAll(childrenIds.map((e) => buildNode(e)).nonNulls); } final node = block?.toNode( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index bbe671a822..b789de3881 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -113,7 +113,7 @@ class TransactionAdapter { ) { return transaction.operations .map((op) => op.toBlockAction(editorState, documentId)) - .whereNotNull() + .nonNulls .expand((element) => element) .toList(growable: false); // avoid lazy evaluation } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 38236b008b..4ebc6f1b47 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,4 +1,4 @@ -library document_plugin; +library; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart index 8fa15af8b2..1be5a41d81 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart @@ -46,7 +46,7 @@ class CollaboratorAvatarStack extends StatelessWidget { width: width, child: WidgetStack( positions: settings, - buildInfoWidget: (value) => plusWidgetBuilder(value, border), + buildInfoWidget: (value, _) => plusWidgetBuilder(value, border), stackedWidgets: avatars .map( (avatar) => CircleAvatar( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 5c3762b438..207951fe22 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -357,7 +357,7 @@ class _AppFlowyEditorPageState extends State ), ), dropTargetStyle: AppFlowyDropTargetStyle( - color: Theme.of(context).colorScheme.primary.withOpacity(0.8), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), margin: const EdgeInsets.only(left: 44), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart index 539dd313b1..a04190f8af 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -110,7 +110,6 @@ class MobileBlockActionButtons extends StatelessWidget { ), ); break; - default: } if (transaction.operations.isNotEmpty) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart index 09bdc06057..cceac56c0d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart @@ -106,7 +106,7 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> { child: FlowyButton( useIntrinsicWidth: true, text: widget.child, - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), onTap: () => controller.show(), ), ); @@ -167,7 +167,7 @@ class _AlignButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyButton( useIntrinsicWidth: true, - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), onTap: onTap, text: FlowyTooltip( message: tooltips, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart index 75f02f5079..4ef680d1b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart @@ -141,7 +141,8 @@ class _FileUploadLocalState extends State<_FileUploadLocal> { height: 32, child: FlowyButton( backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), showDefaultBoxDecorationOnMobile: true, margin: const EdgeInsets.all(5), text: FlowyText( @@ -295,7 +296,7 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> { child: FlowyButton( backgroundColor: Theme.of(context).colorScheme.primary, hoverColor: - Theme.of(context).colorScheme.primary.withOpacity(0.9), + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), showDefaultBoxDecorationOnMobile: true, margin: const EdgeInsets.all(5), text: FlowyText( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart index f716c107df..f4c7a76c0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart @@ -138,7 +138,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> { radius: Corners.s8Border, backgroundColor: Theme.of(context).colorScheme.primary, hoverColor: - Theme.of(context).colorScheme.primary.withOpacity(0.9), + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), margin: const EdgeInsets.all(5), text: FlowyText( LocaleKeys.document_plugins_file_uploadMobileGallery.tr(), @@ -155,7 +155,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> { radius: Corners.s8Border, backgroundColor: Theme.of(context).colorScheme.primary, hoverColor: - Theme.of(context).colorScheme.primary.withOpacity(0.9), + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), margin: const EdgeInsets.all(5), text: FlowyText( LocaleKeys.document_plugins_file_uploadMobile.tr(), @@ -241,7 +241,7 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> { child: FlowyButton( backgroundColor: Theme.of(context).colorScheme.primary, hoverColor: - Theme.of(context).colorScheme.primary.withOpacity(0.9), + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), radius: Corners.s8Border, margin: const EdgeInsets.all(5), text: FlowyText( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart index ef943a7ef7..d297328681 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart @@ -60,7 +60,7 @@ final customizeFontToolbarItem = ToolbarItem( child: FlowyButton( key: kFontFamilyToolbarItemKey, useIntrinsicWidth: true, - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), onTap: () => popoverController.show(), text: const FlowySvg( FlowySvgs.font_family_s, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index a565fd4e43..12fe821459 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -640,7 +640,7 @@ class DocumentCoverState extends State { fillColor: Theme.of(context) .colorScheme .onSurfaceVariant - .withOpacity(0.5), + .withValues(alpha: 0.5), height: 32, title: LocaleKeys.document_plugins_cover_changeCover.tr(), ), @@ -726,8 +726,10 @@ class DocumentCoverState extends State { onPressed: () => popoverController.show(), hoverColor: Theme.of(context).colorScheme.surface, textColor: Theme.of(context).colorScheme.tertiary, - fillColor: - Theme.of(context).colorScheme.surface.withOpacity(0.5), + fillColor: Theme.of(context) + .colorScheme + .surface + .withValues(alpha: 0.5), title: LocaleKeys.document_plugins_cover_changeCover.tr(), ), ), @@ -821,8 +823,8 @@ class DeleteCoverButton extends StatelessWidget { @override Widget build(BuildContext context) { final fillColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withOpacity(0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5); + ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); final svgColor = UniversalPlatform.isDesktopOrWeb ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.onPrimary; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 1d7f43004c..733454d021 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -38,7 +38,10 @@ class _EmojiIconWidgetState extends State { child: Container( decoration: BoxDecoration( color: !hover - ? Theme.of(context).colorScheme.inverseSurface.withOpacity(0.5) + ? Theme.of(context) + .colorScheme + .inverseSurface + .withValues(alpha: 0.5) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), @@ -144,8 +147,6 @@ class RawEmojiIconWidget extends StatelessWidget { height: emojiSize, ), ); - default: - return defaultEmoji; } } catch (e) { Log.error("Display widget error: $e"); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart index c5e1758435..3d0c199ea2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart @@ -140,7 +140,7 @@ class HeadingPopup extends StatelessWidget { }, child: FlowyButton( useIntrinsicWidth: true, - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), text: child, ), ); @@ -209,7 +209,7 @@ class HeadingButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyButton( useIntrinsicWidth: true, - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), onTap: onTap, text: FlowyTooltip( message: tooltip, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index b07e0b3b08..4a6260d8b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -60,7 +60,7 @@ class _ImageMenuState extends State { BoxShadow( blurRadius: 5, spreadRadius: 1, - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), ), ], borderRadius: BorderRadius.circular(4.0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart index 4b53bca127..c0f91a27f0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; @@ -12,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/mult import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/patterns/file_type_patterns.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -24,6 +22,7 @@ import 'package:flowy_infra_ui/style_widget/hover.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:flutter/material.dart'; import 'package:provider/provider.dart'; import '../image_render.dart'; @@ -137,7 +136,8 @@ class _ImageBrowserLayoutState extends State { ), DecoratedBox( decoration: BoxDecoration( - color: Colors.white.withOpacity(0.5), + color: + Colors.white.withValues(alpha: 0.5), ), child: Center( child: FlowyText( @@ -226,8 +226,9 @@ class _ImageBrowserLayoutState extends State { ? const SizedBox.shrink() : SizedBox.expand( child: DecoratedBox( - decoration: - BoxDecoration(color: Colors.white.withOpacity(0.5)), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.5), + ), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -387,8 +388,8 @@ class _ThumbnailItemState extends State { child: FlowyHover( resetHoverOnRebuild: false, style: HoverStyle( - backgroundColor: Colors.black.withOpacity(0.6), - hoverColor: Colors.black.withOpacity(0.9), + backgroundColor: Colors.black.withValues(alpha: 0.6), + hoverColor: Colors.black.withValues(alpha: 0.9), ), child: const Padding( padding: EdgeInsets.all(4), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart index 00919a20cc..43d1c7ae36 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; +import 'package:flutter/material.dart'; abstract class ImageBlockMultiLayout extends StatefulWidget { const ImageBlockMultiLayout({ @@ -65,7 +64,6 @@ class ImageLayoutRender extends StatelessWidget { isLocalMode: isLocalMode, ); case MultiImageLayout.browser: - default: return ImageBrowserLayout( node: node, editorState: editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart index 8abeaf9e99..52bb41fd84 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; @@ -25,6 +22,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; @@ -103,7 +102,7 @@ class _MultiImageMenuState extends State { BoxShadow( blurRadius: 5, spreadRadius: 1, - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), ), ], borderRadius: BorderRadius.circular(4.0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index eb8e8a2707..da37945bf5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -226,7 +226,7 @@ class _ResizableImageState extends State { child: Container( height: 40, decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), borderRadius: const BorderRadius.all( Radius.circular(5.0), ), @@ -262,7 +262,7 @@ class _ImageLoadFailedWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8.0), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), - border: Border.all(color: Colors.grey.withOpacity(0.6)), + border: Border.all(color: Colors.grey.withValues(alpha: 0.6)), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -280,7 +280,7 @@ class _ImageLoadFailedWidget extends StatelessWidget { FlowyText( error, textAlign: TextAlign.center, - color: Theme.of(context).hintColor.withOpacity(0.6), + color: Theme.of(context).hintColor.withValues(alpha: 0.6), fontSize: 10, maxLines: 2, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart index caddbf464a..28dccad72d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart @@ -57,7 +57,8 @@ class _EmbedImageUrlWidgetState extends State { width: 300, child: FlowyButton( backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), showDefaultBoxDecorationOnMobile: true, radius: UniversalPlatform.isMobile ? BorderRadius.circular(8) : null, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index 61e5156060..cf7d72cc2a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -39,7 +39,7 @@ class _LinkPreviewMenuState extends State { BoxShadow( blurRadius: 5, spreadRadius: 1, - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), ), ], borderRadius: BorderRadius.circular(4.0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index a5bc340f71..d65609f1a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -124,8 +124,6 @@ class MentionBlock extends StatelessWidget { reminderOption: reminderOption ?? ReminderOption.none, includeTime: mention[MentionBlockKeys.includeTime] ?? false, ); - default: - return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart index d3d4c5daa9..6ec777429c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart @@ -86,7 +86,7 @@ class _TextColorAndBackgroundColorState EditorTextColorWidget( selectedColor: selectedTextColor?.tryToColor(), onSelectedColor: (textColor) async { - final hex = textColor.alpha == 0 ? null : textColor.toHex(); + final hex = textColor.a == 0 ? null : textColor.toHex(); final selection = widget.selection; if (selection.isCollapsed) { widget.editorState.updateToggledStyle( @@ -123,8 +123,7 @@ class _TextColorAndBackgroundColorState EditorBackgroundColors( selectedColor: selectedBackgroundColor?.tryToColor(), onSelectedColor: (backgroundColor) async { - final hex = - backgroundColor.alpha == 0 ? null : backgroundColor.toHex(); + final hex = backgroundColor.a == 0 ? null : backgroundColor.toHex(); final selection = widget.selection; if (selection.isCollapsed) { widget.editorState.updateToggledStyle( @@ -296,7 +295,7 @@ class _TextColorItem extends StatelessWidget { child: FlowyText( 'A', fontSize: 24, - color: color.alpha == 0 ? null : color, + color: color.a == 0 ? null : color, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart index 376ed90069..112343c6d8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart @@ -468,7 +468,7 @@ class SimpleTableCellBlockWidgetState extends State final isSelectingTable = simpleTableContext?.isSelectingTable.value ?? false; if (isSelectingTable) { - return Theme.of(context).colorScheme.primary.withOpacity(0.1); + return Theme.of(context).colorScheme.primary.withValues(alpha: 0.1); } final columnColor = node.buildColumnColor(context); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart index 6bdbcd516a..79201b38fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart @@ -294,7 +294,7 @@ extension SimpleTableColors on BuildContext { Color get simpleTableDividerColor => Theme.of(this).isLightMode ? const Color(0x141F2329) - : const Color(0xFF23262B).withOpacity(0.5); + : const Color(0xFF23262B).withValues(alpha: 0.5); Color get simpleTableMoreActionBackgroundColor => Theme.of(this).isLightMode ? const Color(0xFFF2F3F5) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart index be82ee3627..4906ed85eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart @@ -412,7 +412,9 @@ class SimpleTableActionMenu extends StatelessWidget { editorState.service.keyboardService?.closeKeyboard(); // delay the bottom sheet show to make sure the keyboard is closed Future.delayed(Durations.short3, () { - _showTableActionBottomSheet(context); + if (context.mounted) { + _showTableActionBottomSheet(context); + } }); }, child: Container( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart index aae1acb68b..b81ff89ee8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart @@ -622,7 +622,7 @@ class _SimpleTableHeaderActionButtonState fit: BoxFit.fill, child: CupertinoSwitch( value: value, - activeColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, onChanged: (_) => _toggle(), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart index 644aae1926..97519422ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart @@ -264,7 +264,7 @@ class _SimpleTableCellBottomSheetState } void _onTextColorSelected(Color color) { - final hex = color.alpha == 0 ? null : color.toHex(); + final hex = color.a == 0 ? null : color.toHex(); switch (widget.type) { case SimpleTableMoreActionType.column: widget.editorState.updateColumnTextColor( @@ -284,7 +284,7 @@ class _SimpleTableCellBottomSheetState } void _onCellBackgroundColorSelected(Color color) { - final hex = color.alpha == 0 ? null : color.toHex(); + final hex = color.a == 0 ? null : color.toHex(); switch (widget.type) { case SimpleTableMoreActionType.column: widget.editorState.updateColumnBackgroundColor( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart index 35ff1f219b..a549e87f83 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart'; @@ -14,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; class SubPageBlockTransactionHandler extends BlockTransactionHandler { @@ -162,7 +161,9 @@ class SubPageBlockTransactionHandler extends BlockTransactionHandler { if (UniversalPlatform.isDesktop) { getIt().openPlugin(view); } else { - await context.pushView(view); + if (context.mounted) { + await context.pushView(view); + } } }); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 31377a93b7..130f9a9ca1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -111,7 +111,8 @@ class EditorStyleCustomizer { fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), + backgroundColor: + theme.colorScheme.inverseSurface.withValues(alpha: 0.8), ), ), ), @@ -159,7 +160,7 @@ class EditorStyleCustomizer { fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: Colors.grey.withOpacity(0.3), + backgroundColor: Colors.grey.withValues(alpha: 0.3), ), ), applyHeightToFirstAscent: true, @@ -241,7 +242,7 @@ class EditorStyleCustomizer { fontFamily: defaultFontFamily, fontSize: fontSize, height: 1.5, - color: AFThemeExtension.of(context).onBackground.withOpacity(0.6), + color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6), ); } @@ -281,7 +282,7 @@ class EditorStyleCustomizer { final afThemeExtension = AFThemeExtension.of(context); return InlineActionsMenuStyle( backgroundColor: theme.cardColor, - groupTextColor: afThemeExtension.onBackground.withOpacity(.8), + groupTextColor: afThemeExtension.onBackground.withValues(alpha: .8), menuItemTextColor: afThemeExtension.onBackground, menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, @@ -470,7 +471,7 @@ class EditorStyleCustomizer { padding: const EdgeInsets.symmetric(vertical: 4.0), child: FlowyHover( style: HoverStyle( - hoverColor: Colors.grey.withOpacity(0.3), + hoverColor: Colors.grey.withValues(alpha: 0.3), ), child: child, ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index be9a6c2f5f..63ccb04839 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -165,7 +165,7 @@ class _InlineActionsHandlerState extends State { BoxShadow( blurRadius: 5, spreadRadius: 1, - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index ff95fe6acc..bf0b7fee26 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -198,7 +198,7 @@ class _ExportButton extends StatelessWidget { Widget build(BuildContext context) { final color = Theme.of(context).isLightMode ? const Color(0x1E14171B) - : Colors.white.withOpacity(0.1); + : Colors.white.withValues(alpha: 0.1); final radius = BorderRadius.circular(10.0); return FlowyButton( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart index 960f59b07d..1c957016e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart @@ -5,7 +5,7 @@ class ShareMenuColors { static Color borderColor(BuildContext context) { final borderColor = Theme.of(context).isLightMode ? const Color(0x1E14171B) - : Colors.white.withOpacity(0.1); + : Colors.white.withValues(alpha: 0.1); return borderColor; } } diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 68d1918c7f..1f754a4372 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -217,7 +217,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> { title: LocaleKeys.shareAction_visitSite.tr(), borderRadius: const BorderRadius.all(Radius.circular(10)), fillColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9), + hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), textColor: Theme.of(context).colorScheme.onPrimary, ); } @@ -508,7 +508,7 @@ class _PublishDatabaseSelector extends StatefulWidget { class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { final PropertyValueNotifier> _databaseStatus = PropertyValueNotifier>([]); - late final _borderColor = Theme.of(context).hintColor.withOpacity(0.3); + late final _borderColor = Theme.of(context).hintColor.withValues(alpha: 0.3); @override void initState() { diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart index 8728c3be4a..40b9c1d6fa 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart @@ -5,7 +5,7 @@ extension PickerColors on BuildContext { Color get pickerTextColor { return Theme.of(this).isLightMode ? const Color(0x80171717) - : Colors.white.withOpacity(0.5); + : Colors.white.withValues(alpha: 0.5); } Color get pickerIconColor { @@ -15,12 +15,12 @@ extension PickerColors on BuildContext { Color get pickerSearchBarBorderColor { return Theme.of(this).isLightMode ? const Color(0x1E171717) - : Colors.white.withOpacity(0.12); + : Colors.white.withValues(alpha: 0.12); } Color get pickerButtonBoarderColor { return Theme.of(this).isLightMode ? const Color(0x1E171717) - : Colors.white.withOpacity(0.12); + : Colors.white.withValues(alpha: 0.12); } } diff --git a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart b/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart index 1e9aa1a3c3..786d666060 100644 --- a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart @@ -1613,7 +1613,7 @@ class _PopupMenuDefaultsM3 extends PopupMenuThemeData { return WidgetStateProperty.resolveWith((Set states) { final TextStyle style = _textTheme.labelLarge!; if (states.contains(WidgetState.disabled)) { - return style.apply(color: _colors.onSurface.withOpacity(0.38)); + return style.apply(color: _colors.onSurface.withValues(alpha: 0.38)); } return style.apply(color: _colors.onSurface); }); diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 920a994927..5bb08e3fdf 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -1,4 +1,4 @@ -library flowy_plugin; +library; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; diff --git a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart index 34925235cb..61694367bb 100644 --- a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart +++ b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart @@ -5,12 +5,17 @@ import 'package:flutter/material.dart'; extension ColorExtension on Color { /// return a hex string in 0xff000000 format String toHexString() { - return '0x${value.toRadixString(16).padLeft(8, '0')}'; + final alpha = (a * 255).toInt().toRadixString(16).padLeft(2, '0'); + final red = (r * 255).toInt().toRadixString(16).padLeft(2, '0'); + final green = (g * 255).toInt().toRadixString(16).padLeft(2, '0'); + final blue = (b * 255).toInt().toRadixString(16).padLeft(2, '0'); + + return '0x$alpha$red$green$blue'.toLowerCase(); } /// return a random color static Color random({double opacity = 1.0}) { return Color((math.Random().nextDouble() * 0xFFFFFF).toInt()) - .withOpacity(opacity); + .withValues(alpha: opacity); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart index d900afd6eb..c3190a8e40 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; /// A class for the default appearance settings for the app class DefaultAppearanceSettings { @@ -15,6 +14,6 @@ class DefaultAppearanceSettings { } static Color getDefaultSelectionColor(BuildContext context) { - return Theme.of(context).colorScheme.primary.withOpacity(0.2); + return Theme.of(context).colorScheme.primary.withValues(alpha: 0.2); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index 37027bcf38..b64ef7d5b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -309,7 +309,6 @@ ThemeModePB _themeModeToPB(ThemeMode themeMode) { case ThemeMode.dark: return ThemeModePB.Dark; case ThemeMode.system: - default: return ThemeModePB.System; } } @@ -358,8 +357,6 @@ enum AppFlowyTextDirection { return TextDirectionPB.RTL; case AppFlowyTextDirection.auto: return TextDirectionPB.AUTO; - default: - return TextDirectionPB.FALLBACK; } } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 63235ba217..6aa649d320 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -49,7 +49,7 @@ class MobileAppearance extends BaseAppearance { error: const Color(0xffFB006D), onError: const Color(0xffFB006D), outline: const Color(0xffe3e3e3), - outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24), + outlineVariant: const Color(0xffCBD5E0).withValues(alpha: 0.24), //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart index 25b51c9a81..af95d5af5a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart @@ -53,8 +53,8 @@ class SettingsShortcutService { } } - /// Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List]. - /// This list needs to be converted to List. This function is intended to facilitate the same. + // Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List]. + // This list needs to be converted to List. This function is intended to facilitate the same. List getShortcutsFromJson(String savedJson) { final shortcuts = EditorShortcuts.fromJson(jsonDecode(savedJson)); return shortcuts.commandShortcuts; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart index deb401ea5c..000de7c9db 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -33,8 +33,8 @@ class RecentViewTile extends StatelessWidget { ), ], ), - focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + focusColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), onTap: () { onSelected(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart index 1db18b77fb..5f26f07597 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -82,7 +82,8 @@ class _SearchResultTileState extends State { child: FlowyHover( isSelected: () => _hasFocus, style: HoverStyle( - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), foregroundColorOnHover: AFThemeExtension.of(context).textColor, ), child: Padding( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index f7a95f3103..ae3b92a702 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -326,7 +326,7 @@ class _SecondaryViewState extends State ? const Color(0x1F1F2329) : Theme.of(context) .shadowColor - .withOpacity(0.08), + .withValues(alpha: 0.08), ), ], ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart index 6d2b93be45..5e1a6f90e0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart @@ -174,8 +174,8 @@ class _PlanIndicatorState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - const Color(0xFF8032FF).withOpacity(.1), - const Color(0xFFEF35FF).withOpacity(.1), + const Color(0xFF8032FF).withValues(alpha: .1), + const Color(0xFFEF35FF).withValues(alpha: .1), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart index 6a23c7def9..716002e917 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart @@ -204,8 +204,6 @@ class _ImportPanelState extends State { ..importType = ImportTypePB.AFDatabase, ); break; - default: - break; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index 7afb4a6298..9b279cc11a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -250,16 +250,16 @@ enum ConfirmPopupStyle { class ConfirmPopupColor { static Color titleColor(BuildContext context) { if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withOpacity(0.8); + return const Color(0xFF171717).withValues(alpha: 0.8); } - return const Color(0xFFffffff).withOpacity(0.8); + return const Color(0xFFffffff).withValues(alpha: 0.8); } static Color descriptionColor(BuildContext context) { if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withOpacity(0.7); + return const Color(0xFF171717).withValues(alpha: 0.7); } - return const Color(0xFFffffff).withOpacity(0.7); + return const Color(0xFFffffff).withValues(alpha: 0.7); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index f3038c0bec..ff393a8305 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -370,7 +370,7 @@ class _CreateWorkspaceButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( - color: const Color(0x01717171).withOpacity(0.12), + color: const Color(0x01717171).withValues(alpha: 0.12), width: 0.8, ), ), @@ -438,7 +438,7 @@ class _ImportNotionButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( - color: const Color(0x01717171).withOpacity(0.12), + color: const Color(0x01717171).withValues(alpha: 0.12), width: 0.8, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 2b27024cff..c604fae432 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -111,7 +111,8 @@ class _DraggableViewItemState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5) + ? widget.centerHighlightColor ?? + hoverColor.withValues(alpha: 0.5) : Colors.transparent, ), child: widget.child, @@ -150,7 +151,10 @@ class _DraggableViewItemState extends State { borderRadius: BorderRadius.circular(4.0), color: position == DraggableHoverPosition.center ? widget.centerHighlightColor ?? - Theme.of(context).colorScheme.secondary.withOpacity(0.5) + Theme.of(context) + .colorScheme + .secondary + .withValues(alpha: 0.5) : Colors.transparent, ), child: widget.child, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 6f1c4920cb..9b2f75ddd0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -76,7 +76,8 @@ class _AccountDeletionButtonState extends State { padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10), fillColor: Colors.transparent, radius: Corners.s8Border, - hoverColor: Theme.of(context).colorScheme.error.withOpacity(0.1), + hoverColor: + Theme.of(context).colorScheme.error.withValues(alpha: 0.1), fontColor: Theme.of(context).colorScheme.error, fontSize: 12, isDangerous: true, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 2230650164..bf1e479226 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -602,8 +602,8 @@ class _PlanProgressIndicator extends StatelessWidget { borderRadius: BorderRadius.circular(8), color: AFThemeExtension.of(context).progressBarBGColor, border: Border.all( - color: const Color(0xFFDDF1F7).withOpacity( - theme.brightness == Brightness.light ? 1 : 0.1, + color: const Color(0xFFDDF1F7).withValues( + alpha: theme.brightness == Brightness.light ? 1 : 0.1, ), ), ), @@ -673,7 +673,7 @@ class _AddOnBox extends StatelessWidget { border: Border.all( color: isActive ? const Color(0xFFBDBDBD) : const Color(0xFF9C00FB), ), - color: const Color(0xFFF7F8FC).withOpacity(0.05), + color: const Color(0xFFF7F8FC).withValues(alpha: 0.05), borderRadius: BorderRadius.circular(16), ), child: Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart index f98ff00a3a..1db39b8fda 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -485,7 +485,7 @@ class KeyBadge extends StatelessWidget { borderRadius: Corners.s4Border, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.25), + color: Colors.black.withValues(alpha: 0.25), blurRadius: 1, offset: const Offset(0, 1), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index ab8d5ee078..1cfc833398 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1090,9 +1090,11 @@ class _FontListPopupState extends State<_FontListPopup> { hoverColor: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.12), - selectedTileColor: - Theme.of(context).colorScheme.primary.withOpacity(0.12), + .withValues(alpha: 0.12), + selectedTileColor: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.12), contentPadding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), minTileHeight: 0, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart index 2ac7814de4..8f9df5f1b6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart @@ -249,7 +249,7 @@ class _FreePlanUpgradeButton extends StatelessWidget { horizontal: 8.0, vertical: 6.0, ), - hoverColor: context.proSecondaryColor.withOpacity(0.9), + hoverColor: context.proSecondaryColor.withValues(alpha: 0.9), onTap: () { if (isOwner) { showToastNotification( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 986e2a4dbd..5165744627 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -171,8 +171,6 @@ class SettingsDialog extends StatelessWidget { ); case SettingsPage.featureFlags: return const FeatureFlagsPage(); - default: - return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart index eb39df8b32..68556f8294 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; class FlowyGradientButton extends StatefulWidget { const FlowyGradientButton({ @@ -49,7 +48,7 @@ class _FlowyGradientButtonState extends State { boxShadow: [ BoxShadow( blurRadius: 4, - color: Colors.black.withOpacity(0.25), + color: Colors.black.withValues(alpha: 0.25), offset: const Offset(0, 2), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 56d2c8d2cc..3b2e883210 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -61,7 +61,7 @@ class _SettingsDropdownState extends State> { const WidgetStatePropertyAll(Size(double.infinity, 250)), elevation: const WidgetStatePropertyAll(10), shadowColor: - WidgetStatePropertyAll(Colors.black.withOpacity(0.4)), + WidgetStatePropertyAll(Colors.black.withValues(alpha: 0.4)), backgroundColor: WidgetStatePropertyAll( Theme.of(context).cardColor, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart index a356e3fd50..6b0c920a04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart @@ -128,11 +128,11 @@ class SingleSettingAction extends StatelessWidget { Color? hoverColor(BuildContext context) { if (buttonType.isDangerous) { - return Theme.of(context).colorScheme.error.withOpacity(0.1); + return Theme.of(context).colorScheme.error.withValues(alpha: 0.1); } if (buttonType.isPrimary) { - return Theme.of(context).colorScheme.primary.withOpacity(0.9); + return Theme.of(context).colorScheme.primary.withValues(alpha: 0.9); } if (buttonType.isHighlight) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart index 22d2bbe034..f329e9dd1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart @@ -3,8 +3,8 @@ 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'; +import 'models/emoji_category_models.dart'; part 'emji_picker_config.freezed.dart'; @@ -87,8 +87,6 @@ class EmojiPickerConfig with _$EmojiPickerConfig { return emojiCategoryIcons.flagIcon; case EmojiCategory.SEARCH: return emojiCategoryIcons.searchIcon; - default: - throw Exception('Unsupported EmojiCategory'); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart index f3cd25afde..ea8ebfe36b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart @@ -16,8 +16,8 @@ class ThemeUploadDecoration extends StatelessWidget { borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), color: Theme.of(context).colorScheme.surface, border: Border.all( - color: AFThemeExtension.of(context).onBackground.withOpacity( - ThemeUploadWidget.fadeOpacity, + color: AFThemeExtension.of(context).onBackground.withValues( + alpha: ThemeUploadWidget.fadeOpacity, ), ), ), @@ -28,7 +28,7 @@ class ThemeUploadDecoration extends StatelessWidget { color: Theme.of(context) .colorScheme .onSurface - .withOpacity(ThemeUploadWidget.fadeOpacity), + .withValues(alpha: ThemeUploadWidget.fadeOpacity), radius: const Radius.circular(ThemeUploadWidget.borderRadius), child: ClipRRect( borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart index edb382d6ee..a7286bee48 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart @@ -15,7 +15,7 @@ class ThemeUploadFailureWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .error - .withOpacity(ThemeUploadWidget.fadeOpacity), + .withValues(alpha: ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), padding: ThemeUploadWidget.padding, child: Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart index 5e0ad15f38..1d3e7ab0f8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart @@ -14,7 +14,7 @@ class ThemeUploadLoadingWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .surface - .withOpacity(ThemeUploadWidget.fadeOpacity), + .withValues(alpha: ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart index 0113d26a37..02a7c8e7ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart @@ -15,7 +15,7 @@ class UploadNewThemeWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .surface - .withOpacity(ThemeUploadWidget.fadeOpacity), + .withValues(alpha: ThemeUploadWidget.fadeOpacity), padding: ThemeUploadWidget.padding, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart index f5c8ffa146..43b3ab9b62 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart @@ -79,9 +79,6 @@ extension QuestionBubbleExtension on SocialMedia { case SocialMedia.forum: return Theme.of(context).hintColor; - - default: - return null; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart index a602aaed58..765a385b0b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart @@ -119,7 +119,7 @@ class InteractiveImageToolbar extends StatelessWidget { child: FlowyHover( resetHoverOnRebuild: false, style: HoverStyle( - hoverColor: Colors.white.withOpacity(0.1), + hoverColor: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Padding( @@ -204,7 +204,7 @@ class InteractiveImageToolbar extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), - color: Colors.black.withOpacity(0.6), + color: Colors.black.withValues(alpha: 0.6), ), child: Padding( padding: const EdgeInsets.all(4), @@ -284,8 +284,9 @@ class _ToolbarItem extends StatelessWidget { child: FlowyHover( resetHoverOnRebuild: false, style: HoverStyle( - hoverColor: - isDisabled ? Colors.transparent : Colors.white.withOpacity(0.1), + hoverColor: isDisabled + ? Colors.transparent + : Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Container( diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 347343dad8..9c0ef58429 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -31,7 +31,7 @@ PODS: - Flutter - FlutterMacOS - ReachabilitySwift (5.2.3) - - screen_retriever (0.0.1): + - screen_retriever_macos (0.0.1): - FlutterMacOS - Sentry/HybridSDK (8.35.1) - sentry_flutter (8.8.0): @@ -43,13 +43,16 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - super_native_extensions (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS @@ -68,13 +71,14 @@ DEPENDENCIES: - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: @@ -112,20 +116,22 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - screen_retriever: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + webview_flutter_wkwebview: + :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos @@ -146,14 +152,15 @@ SPEC CHECKSUMS: package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 share_plus: 1fa619de8392a4398bfaf176d441853922614e89 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift index cad0330b85..14f99e9e07 100644 --- a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart index ebd757aad3..12bfd87ac3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart @@ -23,7 +23,7 @@ class _PopoverMenuState extends State { borderRadius: const BorderRadius.all(Radius.circular(8)), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.5), + color: Colors.grey.withValues(alpha: 0.5), spreadRadius: 5, blurRadius: 7, offset: const Offset(0, 3), // changes position of shadow diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart index 925b9cad02..0b4208e6fc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart @@ -1,5 +1,5 @@ /// AppFlowyBoard library -library appflowy_popover; +library; export 'src/mutex.dart'; export 'src/popover.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart index 97b81cfe1a..d91c9e4954 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart @@ -1,4 +1,5 @@ -library appflowy_result; +/// AppFlowyPopover library +library; export 'src/async_result.dart'; export 'src/result.dart'; diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart index 19ca90d78f..4f80f81e62 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart @@ -13,5 +13,12 @@ class ColorConverter implements JsonConverter { } @override - String toJson(Color color) => "0x${color.value.toRadixString(16)}"; + String toJson(Color color) { + final alpha = (color.a * 255).toInt().toRadixString(16).padLeft(2, '0'); + final red = (color.r * 255).toInt().toRadixString(16).padLeft(2, '0'); + final green = (color.g * 255).toInt().toRadixString(16).padLeft(2, '0'); + final blue = (color.b * 255).toInt().toRadixString(16).padLeft(2, '0'); + + return '0x$alpha$red$green$blue'.toLowerCase(); + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart index 0d4bacde52..fb29bb0637 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; + import 'package:flutter/material.dart'; + import 'flowy_overlay.dart'; class PopoverLayoutDelegate extends SingleChildLayoutDelegate { @@ -133,8 +135,6 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { case AnchorDirection.custom: childConstraints = constraints.loosen(); break; - default: - throw UnimplementedError(); } return childConstraints; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart index 87dd63b715..84e7bb8ebd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; + import 'package:flutter/material.dart'; + import 'flowy_overlay.dart'; class OverlayLayoutDelegate extends SingleChildLayoutDelegate { @@ -133,8 +135,6 @@ class OverlayLayoutDelegate extends SingleChildLayoutDelegate { case AnchorDirection.custom: childConstraints = constraints.loosen(); break; - default: - throw UnimplementedError(); } return childConstraints; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index 2b61839ac3..d1964e0c83 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -128,7 +128,7 @@ class OverlayContainer extends StatelessWidget { padding: padding, decoration: FlowyDecoration.decoration( Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.shadow.withOpacity(0.15), + Theme.of(context).colorScheme.shadow.withValues(alpha: 0.15), ), constraints: constraints, child: child, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart index c227d8b8ef..9a54d51fed 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart @@ -52,8 +52,8 @@ class PrimaryRoundedButton extends StatelessWidget { ), margin: margin ?? const EdgeInsets.symmetric(horizontal: 14.0), backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.primary, - hoverColor: - hoverColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.9), + hoverColor: hoverColor ?? + Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), radius: BorderRadius.circular(radius ?? 10.0), onTap: onTap, ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart index bc391daf9e..95fd82363e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart @@ -1,11 +1,10 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_infra/size.dart'; - class FlowyFormTextInput extends StatelessWidget { static EdgeInsets kDefaultTextInputPadding = const EdgeInsets.only(bottom: 2); @@ -68,7 +67,7 @@ class FlowyFormTextInput extends StatelessWidget { hintStyle: Theme.of(context) .textTheme .bodyMedium! - .copyWith(color: Theme.of(context).hintColor.withOpacity(0.7)), + .copyWith(color: Theme.of(context).hintColor.withValues(alpha: 0.7)), isDense: true, inputBorder: const ThinUnderlineBorder( borderSide: BorderSide(width: 5, color: Colors.red), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart index 8087c712fe..c81c81f356 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart @@ -121,7 +121,7 @@ class BaseStyledBtnState extends State { fillColor: Colors.transparent, hoverColor: widget.hoverColor ?? Colors.transparent, highlightColor: widget.highlightColor ?? Colors.transparent, - focusColor: widget.focusColor ?? Colors.grey.withOpacity(0.35), + focusColor: widget.focusColor ?? Colors.grey.withValues(alpha: 0.35), constraints: BoxConstraints( minHeight: widget.minHeight ?? 0, minWidth: widget.minWidth ?? 0), onPressed: widget.onPressed, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index e29f778d84..7048ed32ec 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -96,7 +96,7 @@ class Dialogs { {required Widget child}) async { return await Navigator.of(context).push( StyledDialogRoute( - barrier: DialogBarrier(color: Colors.black.withOpacity(0.4)), + barrier: DialogBarrier(color: Colors.black.withValues(alpha: 0.4)), pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { return SafeArea(child: child); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index c4c3263d39..a34a55b9f8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -66,7 +66,7 @@ extension FlowyToolTipExtension on BuildContext { } TextStyle? tooltipHintTextStyle({double? fontSize}) => tooltipTextStyle( - fontColor: tooltipFontColor().withOpacity(0.7), + fontColor: tooltipFontColor().withValues(alpha: 0.7), fontSize: fontSize, ); diff --git a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart index cba112dc2b..b5ded7b713 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart @@ -80,7 +80,7 @@ class FlowySvg extends StatelessWidget { Widget build(BuildContext context) { Color? iconColor = color ?? Theme.of(context).iconTheme.color; if (opacity != null) { - iconColor = iconColor?.withOpacity(opacity!); + iconColor = iconColor?.withValues(alpha: opacity!); } final textScaleFactor = MediaQuery.textScalerOf(context).scale(1); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 217c708495..6548cc183d 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.11.0" animations: dependency: transitive description: @@ -37,10 +42,34 @@ packages: dependency: "direct main" description: name: app_links - sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "6.3.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" appflowy_backend: dependency: "direct main" description: @@ -61,11 +90,11 @@ packages: dependency: "direct main" description: path: "." - ref: e4648cc - resolved-ref: e4648ccbc2c1b9ffc5ceb3168b84a9ebdaa692de + ref: "55c457f" + resolved-ref: "55c457f472ed997906bd77142ef94bb7f66cc629" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "4.0.0" + version: "5.0.0" appflowy_editor_plugins: dependency: "direct main" description: @@ -125,10 +154,10 @@ packages: dependency: "direct main" description: name: avatar_stack - sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49 + sha256: "354527ba139956fd6439e2c49199d8298d72afdaa6c4cd6f37f26b97faf21f7e" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "3.0.0" barcode: dependency: transitive description: @@ -213,50 +242,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "8.0.0" built_collection: dependency: transitive description: @@ -342,18 +371,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: "direct main" description: @@ -390,10 +419,10 @@ packages: dependency: transitive description: name: cross_cache - sha256: "3879d1661f211e89d81ece419684df5111b5a611aa6200cd405e8332031765e9" + sha256: "80329477264c73f09945ee780ccdc84df9231f878dc7227d132d301e34ff310b" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" cross_file: dependency: "direct main" description: @@ -422,10 +451,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dbus: dependency: transitive description: @@ -446,10 +475,10 @@ packages: dependency: "direct main" description: name: desktop_drop - sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + sha256: "03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94" url: "https://pub.dev" source: hosted - version: "0.4.4" + version: "0.5.0" device_info_plus: dependency: "direct main" description: @@ -534,18 +563,18 @@ packages: dependency: "direct main" description: name: envied - sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 + sha256: "08a9012e5d93e1a816919a52e37c7b8367e73ebb8d52d1ca7dd6fcd875a2cd2c" url: "https://pub.dev" source: hosted - version: "0.5.4+1" + version: "1.0.1" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a" + sha256: "9a49ca9f3744069661c4f2c06993647699fae2773bca10b593fbb3228d081027" url: "https://pub.dev" source: hosted - version: "0.5.4+1" + version: "1.0.1" equatable: dependency: "direct main" description: @@ -566,10 +595,10 @@ packages: dependency: "direct main" description: name: extended_text_field - sha256: fb5c35460a54906a0ada2a88a968cdfc71d71aebbaf9022debb5d67f47748964 + sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" url: "https://pub.dev" source: hosted - version: "15.0.1" + version: "16.0.2" extended_text_library: dependency: "direct main" description: @@ -654,18 +683,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79" + sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480 url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.7.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" + sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0 url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.5.0" flowy_infra: dependency: "direct main" description: @@ -790,10 +819,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" flutter_localizations: dependency: transitive description: flutter @@ -811,10 +840,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.24" flutter_shaders: dependency: transitive description: @@ -840,21 +869,21 @@ packages: source: hosted version: "0.7.0" flutter_sticky_header: - dependency: transitive + dependency: "direct overridden" description: name: flutter_sticky_header - sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.7.0" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -864,10 +893,10 @@ packages: dependency: "direct main" description: name: flutter_tex - sha256: "816074d8a49dd2301704aaf481f9b052b935b8790018a88b69a60d003d5e89e4" + sha256: ef7896946052e150514a2afe10f6e33e4fe0e7e4fc51195b65da811cb33c59ab url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.0.13" flutter_web_plugins: dependency: transitive description: flutter @@ -885,10 +914,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: @@ -914,10 +943,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 url: "https://pub.dev" source: hosted - version: "7.7.0" + version: "8.0.3" glob: dependency: transitive description: @@ -930,10 +959,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" + sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" url: "https://pub.dev" source: hosted - version: "14.6.2" + version: "14.6.3" google_fonts: dependency: "direct main" description: @@ -1026,10 +1055,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" iconsax_flutter: dependency: transitive description: @@ -1058,10 +1087,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" + sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c url: "https://pub.dev" source: hosted - version: "0.8.12+12" + version: "0.8.12+20" image_picker_for_web: dependency: transitive description: @@ -1074,10 +1103,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" url: "https://pub.dev" source: hosted - version: "0.8.12+1" + version: "0.8.12+2" image_picker_linux: dependency: transitive description: @@ -1098,10 +1127,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.10.1" image_picker_windows: dependency: transitive description: @@ -1175,10 +1204,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" keyboard_height_plugin: dependency: "direct main" description: @@ -1191,18 +1220,18 @@ packages: dependency: "direct main" description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1239,10 +1268,10 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.1.1" loading_indicator: dependency: transitive description: @@ -1275,14 +1304,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" markdown: dependency: "direct main" description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" markdown_widget: dependency: "direct main" description: @@ -1303,34 +1340,34 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: "direct main" description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mockito: dependency: transitive description: name: mockito - sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 url: "https://pub.dev" source: hosted - version: "5.4.4" + version: "5.4.5" mocktail: dependency: "direct dev" description: @@ -1407,10 +1444,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" + sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" package_info_plus_platform_interface: dependency: transitive description: @@ -1455,10 +1492,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.15" path_provider_foundation: dependency: transitive description: @@ -1583,10 +1620,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: "direct dev" description: @@ -1639,10 +1676,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" qr: dependency: transitive description: @@ -1662,10 +1699,11 @@ packages: reorderable_tabbar: dependency: "direct main" description: - name: reorderable_tabbar - sha256: dd19d7b6f60f0dec4be02ba0a2c860f9acbe5a392cb8b5b8c1417cbfcbfe923f - url: "https://pub.dev" - source: hosted + path: "." + ref: "93c4977" + resolved-ref: "93c4977ffab68906694cdeaea262be6045543c94" + url: "https://github.com/LucasXu0/reorderable_tabbar" + source: git version: "1.0.6" reorderables: dependency: "direct main" @@ -1703,10 +1741,42 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" url: "https://pub.dev" source: hosted - version: "0.1.9" + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" scroll_to_index: dependency: "direct main" description: @@ -1743,10 +1813,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "10.1.3" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: @@ -1759,18 +1829,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_foundation: dependency: transitive description: @@ -1824,10 +1894,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -1848,10 +1918,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" simple_gesture_detector: dependency: transitive description: @@ -1872,7 +1942,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" sliver_tools: dependency: transitive description: @@ -1933,26 +2003,50 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1973,10 +2067,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" string_validator: dependency: "direct main" description: @@ -2021,10 +2115,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" tab_indicator_styler: dependency: transitive description: @@ -2053,26 +2147,26 @@ packages: dependency: transitive description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.5" time: dependency: "direct main" description: @@ -2109,10 +2203,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_html: dependency: transitive description: @@ -2157,10 +2251,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.14" url_launcher_ios: dependency: transitive description: @@ -2197,18 +2291,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" url_protocol: dependency: "direct main" description: @@ -2230,10 +2324,10 @@ packages: dependency: transitive description: name: value_layout_builder - sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.4.0" vector_graphics: dependency: transitive description: @@ -2246,10 +2340,10 @@ packages: dependency: transitive description: name: vector_graphics_codec - sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.12" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: @@ -2278,10 +2372,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" watcher: dependency: transitive description: @@ -2298,22 +2392,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webdriver: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: @@ -2326,18 +2428,18 @@ packages: dependency: transitive description: name: webview_flutter - sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" + sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.10.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: ed021f27ae621bc97a6019fb601ab16331a3db4bf8afa305e9f6689bdb3edced + sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f url: "https://pub.dev" source: hosted - version: "3.16.8" + version: "4.3.0" webview_flutter_platform_interface: dependency: transitive description: @@ -2350,26 +2452,26 @@ packages: dependency: transitive description: name: webview_flutter_plus - sha256: "57ec757eada4e23bfb015f5d5f84a45108cb2c29b1e77e23956768cd5e0c8468" + sha256: f883dfc94d03b1a2a17441c8e8a8e1941558ed3322f2b586cd06486114e18048 url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.10" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" + sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7" url: "https://pub.dev" source: hosted - version: "3.14.0" + version: "3.17.0" win32: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.0" win32_registry: dependency: transitive description: @@ -2382,10 +2484,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" url: "https://pub.dev" source: hosted - version: "0.3.9" + version: "0.4.3" xdg_directories: dependency: transitive description: @@ -2411,5 +2513,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.4" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index ec11e4dca7..5390446e8f 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -7,12 +7,12 @@ publish_to: "none" version: 0.8.2 environment: - flutter: ">=3.22.0" + flutter: ">=3.27.4" sdk: ">=3.3.0 <4.0.0" dependencies: any_date: ^1.0.4 - app_links: ^3.5.0 + app_links: ^6.3.3 appflowy_backend: path: packages/appflowy_backend appflowy_board: @@ -28,7 +28,7 @@ dependencies: archive: ^3.4.10 auto_size_text_field: ^2.2.3 - avatar_stack: ^1.2.0 + avatar_stack: ^3.0.0 # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 @@ -44,15 +44,15 @@ dependencies: # Desktop Drop uses Cross File (XFile) data type defer_pointer: ^0.0.2 - desktop_drop: ^0.4.4 + desktop_drop: ^0.5.0 device_info_plus: diffutil_dart: ^4.0.1 dotted_border: ^2.0.0+3 easy_localization: ^3.0.2 - envied: ^0.5.2 + envied: ^1.0.1 equatable: ^2.0.5 expandable: ^5.0.1 - extended_text_field: ^15.0.0 + extended_text_field: ^16.0.2 extended_text_library: ^12.0.0 file: ^7.0.0 fixnum: ^1.1.0 @@ -68,8 +68,8 @@ dependencies: flutter_animate: ^4.5.0 flutter_bloc: ^8.1.3 flutter_cache_manager: ^3.3.1 - flutter_chat_core: ^0.0.2 - flutter_chat_ui: 2.0.0-dev.1 + flutter_chat_core: 0.0.2 + flutter_chat_ui: ^2.0.0-dev.1 flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git @@ -81,7 +81,7 @@ dependencies: flutter_tex: ^4.0.9 fluttertoast: ^8.2.6 freezed_annotation: ^2.2.0 - get_it: ^7.6.0 + get_it: ^8.0.3 go_router: ^14.2.0 google_fonts: ^6.1.0 highlight: ^0.7.0 @@ -104,7 +104,7 @@ dependencies: local_notifier: ^0.1.5 markdown: markdown_widget: ^2.3.2+6 - mime: ^1.0.6 + mime: ^2.0.0 nanoid: ^1.0.0 numerus: ^2.1.2 @@ -113,7 +113,7 @@ dependencies: package_info_plus: ^8.0.2 path: ^1.8.3 path_provider: ^2.0.15 - percent_indicator: ^4.2.3 + percent_indicator: 4.2.3 permission_handler: ^11.3.1 protobuf: ^3.1.0 provider: ^6.0.5 @@ -142,13 +142,13 @@ dependencies: url_protocol: # Window Manager for MacOS and Linux - window_manager: ^0.3.9 + window_manager: ^0.4.3 dev_dependencies: bloc_test: ^9.1.2 build_runner: ^2.4.9 - envied_generator: ^0.5.2 - flutter_lints: ^4.0.0 + envied_generator: ^1.0.1 + flutter_lints: ^5.0.0 flutter_test: sdk: flutter @@ -175,7 +175,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "e4648cc" + ref: "55c457f" appflowy_editor_plugins: git: @@ -197,6 +197,12 @@ dependency_overrides: commit: fbab857b1b1d209240a146d32f496379b9f62276 path: flutter_cache_manager + flutter_sticky_header: ^0.7.0 + + reorderable_tabbar: + git: + url: https://github.com/LucasXu0/reorderable_tabbar + ref: 93c4977 # Don't upgrade file_picker until the issue is fixed # https://github.com/miguelpruivo/flutter_file_picker/issues/1652 file_picker: 8.1.4 diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 960c1ba625..9e422f80ca 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -29,7 +29,7 @@ RUN source ~/.cargo/env && \ RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.22.0-stable.tar.xz && \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.27.4-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 004fa6fc0b..1c31696f39 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.22.0 -if [ "$FLUTTER_VERSION" = "3.22.0" ]; then - echo "Flutter version is already 3.22.0" +# Check if the current version is 3.27.4 +if [ "$FLUTTER_VERSION" = "3.27.4" ]; then + echo "Flutter version is already 3.27.4" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.22.0 of Flutter - git checkout 3.22.0 + # Use git to checkout version 3.27.4 of Flutter + git checkout 3.27.4 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.22.0" + echo "Switched to Flutter version 3.27.4" fi # Enable linux desktop @@ -85,12 +85,16 @@ cd frontend || exit 1 # Install cargo make printMessage "Installing cargo-make." -cargo install --force cargo-make +cargo install --force --locked cargo-make # Install duckscript printMessage "Installing duckscript." cargo install --force --locked duckscript_cli +# Install cargo-lipo +printMessage "Installing cargo-lipo." +cargo install --force --locked cargo-lipo + # Check prerequisites printMessage "Checking prerequisites." cargo make appflowy-flutter-deps-tools diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index 1028605da9..57db01a73d 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.22.0 -if [ "$FLUTTER_VERSION" = "3.22.0" ]; then - echo "Flutter version is already 3.22.0" +# Check if the current version is 3.27.4 +if [ "$FLUTTER_VERSION" = "3.27.4" ]; then + echo "Flutter version is already 3.27.4" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.22.0 of Flutter - git checkout 3.22.0 + # Use git to checkout version 3.27.4 of Flutter + git checkout 3.27.4 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.22.0" + echo "Switched to Flutter version 3.27.4" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 27ac7ad9d1..463071e2af 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.22.0 -if [ "$FLUTTER_VERSION" = "3.22.0" ]; then - echo "Flutter version is already 3.22.0" +# Check if the current version is 3.27.4 +if [ "$FLUTTER_VERSION" = "3.27.4" ]; then + echo "Flutter version is already 3.27.4" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.22.0 of Flutter - git checkout 3.22.0 + # Use git to checkout version 3.27.4 of Flutter + git checkout 3.27.4 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.22.0" + echo "Switched to Flutter version 3.27.4" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index 9ff586668c..3cceec5bb0 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.22.0 -if [ "$FLUTTER_VERSION" = "3.22.0" ]; then - echo "Flutter version is already 3.22.0" +# Check if the current version is 3.27.4 +if [ "$FLUTTER_VERSION" = "3.27.4" ]; then + echo "Flutter version is already 3.27.4" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.22.0 of Flutter - git checkout 3.22.0 + # Use git to checkout version 3.27.4 of Flutter + git checkout 3.27.4 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.22.0" + echo "Switched to Flutter version 3.27.4" fi # Add pub cache and cargo to PATH From fc9c152553fc0ce8669f37c78edc8baa19f0e4dd Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:44:55 +0800 Subject: [PATCH 018/384] fix(flutter_desktop): selection in AI chat going missing while scrolling (#7281) --- .../lib/plugins/ai_chat/chat_page.dart | 33 +++++++------ .../message/user_text_message.dart | 1 - .../flowy_infra_ui/lib/style_widget/text.dart | 48 ++++++------------- 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 067ba95b6b..650840e019 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -121,21 +121,24 @@ class _ChatContentPage extends StatelessWidget { child: Align( alignment: Alignment.topCenter, child: _wrapConstraints( - ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: Chat( - chatController: - context.read().chatController, - user: User(id: userProfile.id.toString()), - darkTheme: ChatTheme.fromThemeData(Theme.of(context)), - theme: ChatTheme.fromThemeData(Theme.of(context)), - builders: Builders( - inputBuilder: (_) => const SizedBox.shrink(), - textMessageBuilder: _buildTextMessage, - chatMessageBuilder: _buildChatMessage, - scrollToBottomBuilder: _buildScrollToBottom, - chatAnimatedListBuilder: _buildChatAnimatedList, + SelectionArea( + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: Chat( + chatController: + context.read().chatController, + user: User(id: userProfile.id.toString()), + darkTheme: + ChatTheme.fromThemeData(Theme.of(context)), + theme: ChatTheme.fromThemeData(Theme.of(context)), + builders: Builders( + inputBuilder: (_) => const SizedBox.shrink(), + textMessageBuilder: _buildTextMessage, + chatMessageBuilder: _buildChatMessage, + scrollToBottomBuilder: _buildScrollToBottom, + chatAnimatedListBuilder: _buildChatAnimatedList, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index ae0d2863ce..3638ace12f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -83,7 +83,6 @@ class TextMessageText extends StatelessWidget { text, lineHeight: 1.4, maxLines: null, - selectable: true, color: AFThemeExtension.of(context).textColor, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 76e9eefbc2..360578a4a6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -15,7 +15,6 @@ class FlowyText extends StatelessWidget { final TextDecoration? decoration; final Color? decorationColor; final double? decorationThickness; - final bool selectable; final String? fontFamily; final List? fallbackFontFamily; final bool withTooltip; @@ -41,7 +40,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.fontFamily, this.fallbackFontFamily, // // https://api.flutter.dev/flutter/painting/TextStyle/height.html @@ -63,7 +61,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, @@ -86,7 +83,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, @@ -108,7 +104,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, @@ -130,7 +125,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, @@ -153,7 +147,6 @@ class FlowyText extends StatelessWidget { this.maxLines = 1, this.decoration, this.decorationColor, - this.selectable = false, this.lineHeight, this.withTooltip = false, this.strutStyle = const StrutStyle(forceStrutHeight: true), @@ -211,32 +204,21 @@ class FlowyText extends StatelessWidget { : null, ); - if (selectable) { - child = IntrinsicHeight( - child: SelectableText( - text, - maxLines: maxLines, - textAlign: textAlign, - style: textStyle, - ), - ); - } else { - child = Text( - text, - maxLines: maxLines, - textAlign: textAlign, - overflow: overflow ?? TextOverflow.clip, - style: textStyle, - strutStyle: !isEmoji || (isEmoji && optimizeEmojiAlign) - ? StrutStyle.fromTextStyle( - textStyle, - forceStrutHeight: true, - leadingDistribution: TextLeadingDistribution.even, - height: lineHeight, - ) - : null, - ); - } + child = Text( + text, + maxLines: maxLines, + textAlign: textAlign, + overflow: overflow ?? TextOverflow.clip, + style: textStyle, + strutStyle: !isEmoji || (isEmoji && optimizeEmojiAlign) + ? StrutStyle.fromTextStyle( + textStyle, + forceStrutHeight: true, + leadingDistribution: TextLeadingDistribution.even, + height: lineHeight, + ) + : null, + ); if (withTooltip) { child = FlowyTooltip( From f53e9d6549692a4c823c3c4ea7f681cd5e55377e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 10 Feb 2025 09:20:24 +0800 Subject: [PATCH 019/384] feat: integrate auto_updater for macOS (#7328) * feat: integrate auto_updater in macOS * chore: update translations * chore: bump auto_updater version * feat: exclude linux platform in auto update task * chore: disable auto updater * fix: integration tests * fix: integration tests --- frontend/appflowy_flutter/dsa_pub.pem | 36 ++++ .../anon_user_data_migration_test.dart | 6 - .../appflowy_cloud_auth_test.dart | 6 - frontend/appflowy_flutter/ios/Podfile.lock | 44 ++-- .../lib/startup/deps_resolver.dart | 4 +- .../appflowy_flutter/lib/startup/startup.dart | 3 +- .../lib/startup/tasks/auto_update_task.dart | 188 ++++++++++++++++++ .../lib/startup/tasks/device_info_task.dart | 56 +++++- .../lib/startup/tasks/prelude.dart | 2 + .../sidebar_upgarde_application_button.dart | 110 ++++++++++ .../home/menu/sidebar/sidebar.dart | 43 ++++ .../menu/sidebar/space/shared_widget.dart | 46 +++-- .../settings/pages/about/app_version.dart | 147 ++++++++++++++ .../pages/account/account_sign_in_out.dart | 34 +++- .../settings/pages/settings_account_view.dart | 44 ++-- .../presentation/widgets/dialogs.dart | 6 + frontend/appflowy_flutter/macos/Podfile.lock | 62 +++--- .../macos/Runner/AppDelegate.swift | 4 + .../macos/Runner/DebugProfile.entitlements | 34 ++-- .../appflowy_flutter/macos/Runner/Info.plist | 108 +++++----- .../macos/Runner/Release.entitlements | 34 ++-- .../appflowy_backend/example/macos/Podfile | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 25 +-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../style_widget/primary_rounded_button.dart | 1 + frontend/appflowy_flutter/pubspec.lock | 51 ++++- frontend/appflowy_flutter/pubspec.yaml | 29 +++ .../appflowy_flutter/windows/runner/Runner.rc | 8 + .../16x/sidebar_upgrade_version.svg | 9 + frontend/resources/translations/en.json | 13 ++ 30 files changed, 950 insertions(+), 207 deletions(-) create mode 100644 frontend/appflowy_flutter/dsa_pub.pem create mode 100644 frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart create mode 100644 frontend/resources/flowy_icons/16x/sidebar_upgrade_version.svg diff --git a/frontend/appflowy_flutter/dsa_pub.pem b/frontend/appflowy_flutter/dsa_pub.pem new file mode 100644 index 0000000000..b4522087e3 --- /dev/null +++ b/frontend/appflowy_flutter/dsa_pub.pem @@ -0,0 +1,36 @@ +-----BEGIN PUBLIC KEY----- +MIIGQjCCBDUGByqGSM44BAEwggQoAoICAQCJsCIhS7IK+c4R3GVBuJBjI0/gW19n +flfKzC0kdt6HUtyirJ/v8SafqwRWSkbPVcrdVvHHBXOs2v7JogyZuy9v8SRvpW1s +trR6hFExtllYTo8uLi1YE5LGt4SwjduRYpwPvGvSxU5An8yESu/96JShQekHVPTj +ILfDNOOP2iRVIwRRVNZfFT/XOX5mN4RysUd+0KZ5IBR1LzRgw2O02sHheMkK/Uqr +19Of5eC1JTqgki9hHdjtjTsohJCrecFPDg4ej5Ac7HrbsGEklDDwtd7ftit2mnuz +ZNhG9qpfNztz9TQ6HCQCCrQCm1H7BkQDg5y7qFC9bARjMiQh0ygic97USeQLDu3l +EyxPAbIvo4m4IdHpaQSYRkse24y3b+k8BrQ9qNm4ElTIjEt4rs3Ic9aJ2j62qugq +7q+3EA0aDHbsB+TbjMm4sW3G9NVsr88nk1UUCayLDyCHwaXVB5KCfrVgM/5r/Avl +2ukkDdIB3A5gnO5+MY5fVS4WgxCPuAfJ6vp9/r2U1dGRDUAaISqg44uOwppXTnoZ +JQ3ZxZmyF6BVwvpjrZ7B8TfPco2JwBFI33W9byOWckUtc2cLAbuPutR1ymMp1F8J +B4G0VbpzT2liGaHqBV82J5+4ljAhcUnEzFOJ4ysDaJ0/n3NQuNGb2EX6SF9E5A2A +eaeEu5C0MWSFLQIdAJJP3sJsHh/raF/aqWjwKQQ2NWom4fKH1v7OSIcCggIAGn3W +SUWgyINiLoahtDZ2zkL7oMDQwZCC7lGUTyhi9gDnllNtJkpBpyEvgU479MnpGpgp +IlheOHYrEtL3EM31KwKvRism4TMkNd5+ZFw6//AcuoRd0YcGisk/UdE9PlhoKvLu +pzlQLf9iCeFtevW0TgHp/su0ZP6tSb1p/97vJOO5qY8gE4bCUNknlovFuhCwI9Al +eaQjhoDDDhimnkC8ZMsybvri9ZtNe24erZDaD1eQ85U0zR8CR73wFo1osDBjGgeK +cngcnQXoPfpj1tgmd1lEAcO8qD6BzZkEVyN+fgeqj22nfwqDDe9sXIEvf1KkhKEy +zWsdoZ6LCgWfI6Iqs4mqYlJPRhGFZ6hq0tncBzoxZcTeX1EYRhgYUeBe91CRlHMs +SIfsW5fpjWZ2QeDAsr9qmJ+RK06bN6U65o2nSOh0jaJpjT0VF6VdLLDF/bGrIjey +oEHg7b/SO8z7zO5H6CnHF4m4TG/EfkD5CSlT2SGer7yEF5KYeG5QX0W15ELTghak +IRsvBT6D8MWKjnfuxJjKLQoXPiw9Ua+nhG4YyQpDey1e7Y2oUFONdFRR38ocZuqQ +7YyxUzsQPLs1oEqfLm09gaF9gV/yPY09ocV2t+dO6PRq6BHWkdCdipUsFAwcPxmV +r4c8HCXKvlpNHDDu7Lw2CkfQdCbqpuT7gqsz+WgDggIFAAKCAgBOv5yBTT5SuSax +SmWwsoorMV+CxLRj0SM2T2FW9YK354BpDm95rpQZ4Za9m2wj59i73j/HboREsKgd +FzajWEd2S9VSHJ4dYx4m3wkQ6BFN/ryVocmG2PzAistXAOBB32dnQkK5cwDxQn2R ++rKHSSWtuJGv2YrwbxdpZM0zvOHvjYEXndV2XdH/DW4j9DivuCNoJlzaOvyXQsuE +jpcB0tg0QfZ564/6MIbzUivo7RKiLnmFiIksKymDYwp49382EWoSvF+rTjpqs1zH ++7jyva1m14jTgdSDjdoZakpkNpJpyN3Z6whQczBL0g3T3t3VEN6F7NGA0aZVrSkO +rPYXvZdEtsH4qGpb9SQx2fbE3RBD2LcxWxFBk0SlzF7RiCzolre6v/kqfJA5oejA +qTyTJpadheUn/+yYltnzCinILrDYwHzPAZWUXc1guRrdRRcv+d1gW3VG5n37NdYh +lSgowVOUfTcZv6nCr+Ohbirmogc/crcBVUhSxLufajKKMtzJBwbZ8yvUtgmCXCZ7 +6kVYIoRuQCkzJvN50dM9Jd20+e2b0MXdj8fEMF9UnBIqAklStMpkaK5dFXLUDHho +uG9cYnd4HIeLHZvfLgQ+DrBBdvLKPBez3vkIofocQDP4KIS4Dra4lLtDA07UmSwY +p6Dfx6Mqf0+pSpyZcf+qW1vlk3q92g== +-----END PUBLIC KEY----- diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart index 06c27093f9..230ee59495 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart @@ -31,12 +31,6 @@ void main() { await tester.enterUserName('local_user'); // Scroll to sign-in - await tester.scrollUntilVisible( - find.byType(AccountSignInOutButton), - 100, - scrollable: find.findSettingsScrollable(), - ); - await tester.tapButton(find.byType(AccountSignInOutButton)); // sign up with Google diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart index 0b649fd6d5..fd65c29927 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart @@ -57,12 +57,6 @@ void main() { await tester.openSettings(); await tester.openSettingsPage(SettingsPage.account); - // Scroll to sign-in - await tester.scrollUntilVisible( - find.byType(AccountSignInOutButton), - 100, - scrollable: find.findSettingsScrollable(), - ); await tester.tapButton(find.byType(AccountSignInOutButton)); tester.expectToSeeGoogleLoginButton(); diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 46b13d240d..e4e87805cd 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -175,36 +175,36 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 - appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 + appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 - flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 - keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a + irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 + super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index a0321e7e66..64747b4829 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,15 +1,15 @@ +import 'package:appflowy/ai/service/ai_client.dart'; +import 'package:appflowy/ai/service/appflowy_ai_service.dart'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/cloud_env.dart'; 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/ai/service/ai_client.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; -import 'package:appflowy/ai/service/appflowy_ai_service.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/prelude.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index ac0be447c0..7fa18fc54d 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/startup/tasks/feature_flag_task.dart'; import 'package:appflowy/util/expand_views.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; @@ -139,6 +138,8 @@ class FlowyRunner { // The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information. // It is unable to get the device information from the test environment. const ApplicationInfoTask(), + // The auto update task should be placed after the ApplicationInfoTask to fetch the latest version. + if (!mode.isIntegrationTest) AutoUpdateTask(), const HotKeyTask(), if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), const InitAppWidgetTask(), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart new file mode 100644 index 0000000000..3371abf224 --- /dev/null +++ b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart @@ -0,0 +1,188 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:auto_updater/auto_updater.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:universal_platform/universal_platform.dart'; + +import '../startup.dart'; + +class AutoUpdateTask extends LaunchTask { + AutoUpdateTask(); + + // static const _feedUrl = + // 'https://github.com/LucasXu0/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml'; + final _listener = _AppFlowyAutoUpdaterListener(); + + @override + Future initialize(LaunchContext context) async { + // Enable auto update when the integration of Windows and Linux is completed. + return; + // // the auto updater is not supported on mobile and linux + // if (UniversalPlatform.isMobile || UniversalPlatform.isLinux) { + // return; + // } + + // Log.info( + // '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}', + // ); + + // autoUpdater.addListener(_listener); + + // // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch. + // final feedUrl = _feedUrl + // .replaceAll('{os}', ApplicationInfo.os) + // .replaceAll('{arch}', ApplicationInfo.architecture); + // Log.info('[AutoUpdate] feed url: $feedUrl'); + + // await autoUpdater.setFeedURL(feedUrl); + // await autoUpdater.checkForUpdateInformation(); + + // ApplicationInfo.isCriticalUpdateNotifier.addListener( + // _showCriticalUpdateDialog, + // ); + } + + @override + Future dispose() async { + autoUpdater.removeListener(_listener); + + ApplicationInfo.isCriticalUpdateNotifier.removeListener( + _showCriticalUpdateDialog, + ); + } + + void _showCriticalUpdateDialog() { + showCustomConfirmDialog( + context: AppGlobals.rootNavKey.currentContext!, + title: LocaleKeys.autoUpdate_criticalUpdateTitle.tr(), + description: LocaleKeys.autoUpdate_criticalUpdateDescription.tr( + namedArgs: { + 'currentVersion': ApplicationInfo.applicationVersion, + 'newVersion': ApplicationInfo.latestVersion, + }, + ), + builder: (context) => const SizedBox.shrink(), + // if the update is critical, dont allow the user to dismiss the dialog + barrierDismissible: false, + showCloseButton: false, + enableKeyboardListener: false, + closeOnConfirm: false, + confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(), + onConfirm: () async { + await autoUpdater.checkForUpdates(); + }, + ); + } +} + +class _AppFlowyAutoUpdaterListener extends UpdaterListener { + @override + void onUpdaterBeforeQuitForUpdate(AppcastItem? item) {} + + @override + void onUpdaterCheckingForUpdate(Appcast? appcast) { + // Due to the reason documented in the following link, the update will not be found if the user has skipped the update. + // We have to check the skipped version manually. + // https://sparkle-project.org/documentation/api-reference/Classes/SPUUpdater.html#/c:objc(cs)SPUUpdater(im)checkForUpdateInformation + final items = appcast?.items; + if (items != null) { + final String? currentPlatform; + if (UniversalPlatform.isMacOS) { + currentPlatform = 'macos'; + } else if (UniversalPlatform.isWindows) { + currentPlatform = 'windows'; + } else { + currentPlatform = null; + } + + final matchingItem = items.firstWhereOrNull( + (item) => item.os == currentPlatform, + ); + + if (matchingItem != null) { + _updateVersionNotifier(matchingItem); + + Log.info( + '[AutoUpdate] latest version: ${matchingItem.displayVersionString}', + ); + } + } + } + + @override + void onUpdaterError(UpdaterError? error) { + Log.error('[AutoUpdate] On update error: $error'); + } + + @override + void onUpdaterUpdateNotAvailable(UpdaterError? error) { + Log.info('[AutoUpdate] Update not available $error'); + } + + @override + void onUpdaterUpdateAvailable(AppcastItem? item) { + _updateVersionNotifier(item); + + Log.info('[AutoUpdate] Update available: ${item?.displayVersionString}'); + } + + @override + void onUpdaterUpdateDownloaded(AppcastItem? item) { + Log.info('[AutoUpdate] Update downloaded: ${item?.displayVersionString}'); + } + + @override + void onUpdaterUpdateCancelled(AppcastItem? item) { + _updateVersionNotifier(item); + + Log.info('[AutoUpdate] Update cancelled: ${item?.displayVersionString}'); + } + + @override + void onUpdaterUpdateInstalled(AppcastItem? item) { + _updateVersionNotifier(item); + + Log.info('[AutoUpdate] Update installed: ${item?.displayVersionString}'); + } + + @override + void onUpdaterUpdateSkipped(AppcastItem? item) { + _updateVersionNotifier(item); + + Log.info('[AutoUpdate] Update skipped: ${item?.displayVersionString}'); + } + + void _updateVersionNotifier(AppcastItem? item) { + if (item != null) { + ApplicationInfo.latestAppcastItem = item; + ApplicationInfo.latestVersionNotifier.value = + item.displayVersionString ?? ''; + } + } +} + +class AppFlowyAutoUpdateVersion { + AppFlowyAutoUpdateVersion({ + required this.latestVersion, + required this.currentVersion, + required this.isForceUpdate, + }); + + factory AppFlowyAutoUpdateVersion.initial() => AppFlowyAutoUpdateVersion( + latestVersion: '0.0.0', + currentVersion: '0.0.0', + isForceUpdate: false, + ); + + final String latestVersion; + final String currentVersion; + + final bool isForceUpdate; + + bool get isUpdateAvailable => latestVersion != currentVersion; +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart index 1558eefa53..81464f59d7 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart @@ -1,8 +1,11 @@ import 'dart:io'; import 'package:appflowy_backend/log.dart'; +import 'package:auto_updater/auto_updater.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:version/version.dart'; import '../startup.dart'; @@ -11,10 +14,39 @@ class ApplicationInfo { static String applicationVersion = ''; static String buildNumber = ''; static String deviceId = ''; + static String architecture = ''; + static String os = ''; // macOS major version static int? macOSMajorVersion; static int? macOSMinorVersion; + + // latest version + static ValueNotifier latestVersionNotifier = ValueNotifier(''); + // the version number is like 0.9.0 + static String get latestVersion => latestVersionNotifier.value; + + // If the latest version is greater than the current version, it means there is an update available + static bool get isUpdateAvailable { + try { + return Version.parse(latestVersion) > Version.parse(applicationVersion); + } catch (e) { + return false; + } + } + + // the latest appcast item + static AppcastItem? _latestAppcastItem; + static AppcastItem? get latestAppcastItem => _latestAppcastItem; + static set latestAppcastItem(AppcastItem? value) { + _latestAppcastItem = value; + + isCriticalUpdateNotifier.value = value?.criticalUpdate == true; + } + + // is critical update + static ValueNotifier isCriticalUpdateNotifier = ValueNotifier(false); + static bool get isCriticalUpdate => isCriticalUpdateNotifier.value; } class ApplicationInfoTask extends LaunchTask { @@ -36,38 +68,54 @@ class ApplicationInfoTask extends LaunchTask { ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt; } - if (Platform.isAndroid || Platform.isIOS) { - ApplicationInfo.applicationVersion = packageInfo.version; - ApplicationInfo.buildNumber = packageInfo.buildNumber; - } + ApplicationInfo.applicationVersion = packageInfo.version; + ApplicationInfo.buildNumber = packageInfo.buildNumber; String? deviceId; + String? architecture; + String? os; try { if (Platform.isAndroid) { final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; deviceId = androidInfo.device; + architecture = androidInfo.supportedAbis.firstOrNull; + os = 'android'; } else if (Platform.isIOS) { final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; deviceId = iosInfo.identifierForVendor; + architecture = iosInfo.utsname.machine; + os = 'ios'; } else if (Platform.isMacOS) { final MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo; deviceId = macInfo.systemGUID; + architecture = macInfo.arch; + os = 'macos'; } else if (Platform.isWindows) { final WindowsDeviceInfo windowsInfo = await deviceInfoPlugin.windowsInfo; deviceId = windowsInfo.deviceId; + // we only support x86_64 on Windows + architecture = 'x86_64'; + os = 'windows'; } else if (Platform.isLinux) { final LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo; deviceId = linuxInfo.machineId; + // we only support x86_64 on Linux + architecture = 'x86_64'; + os = 'linux'; } else { deviceId = null; + architecture = null; + os = null; } } catch (e) { Log.error('Failed to get platform version, $e'); } ApplicationInfo.deviceId = deviceId ?? ''; + ApplicationInfo.architecture = architecture ?? ''; + ApplicationInfo.os = os ?? ''; } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 4be5f0f6f7..9e8f9df49a 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -1,7 +1,9 @@ export 'app_widget.dart'; export 'appflowy_cloud_task.dart'; +export 'auto_update_task.dart'; export 'debug_task.dart'; export 'device_info_task.dart'; +export 'feature_flag_task.dart'; export 'generate_router.dart'; export 'hot_key.dart'; export 'load_plugin.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart new file mode 100644 index 0000000000..abe3ffd354 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart @@ -0,0 +1,110 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class SidebarUpgradeApplicationButton extends StatelessWidget { + const SidebarUpgradeApplicationButton({ + super.key, + required this.onUpdateButtonTap, + required this.onCloseButtonTap, + }); + + final VoidCallback onUpdateButtonTap; + final VoidCallback onCloseButtonTap; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.sidebarUpgradeButtonBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // title + _buildTitle(), + const VSpace(2), + // description + _buildDescription(), + const VSpace(10), + // update button + _buildUpdateButton(), + ], + ), + ); + } + + Widget _buildTitle() { + return Row( + children: [ + const FlowySvg( + FlowySvgs.sidebar_upgrade_version_s, + blendMode: null, + ), + const HSpace(6), + FlowyText.medium( + LocaleKeys.autoUpdate_bannerUpdateTitle.tr(), + fontSize: 14, + figmaLineHeight: 18, + ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.upgrade_close_s), + onTap: onCloseButtonTap, + ), + ], + ); + } + + Widget _buildDescription() { + return Opacity( + opacity: 0.7, + child: FlowyText( + LocaleKeys.autoUpdate_bannerUpdateDescription.tr(), + fontSize: 13, + figmaLineHeight: 16, + maxLines: null, + ), + ); + } + + Widget _buildUpdateButton() { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onUpdateButtonTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: ShapeDecoration( + color: const Color(0xFFA44AFD), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + ), + ), + child: FlowyText.medium( + LocaleKeys.autoUpdate_settingsUpdateButton.tr(), + color: Colors.white, + fontSize: 12.0, + figmaLineHeight: 15.0, + ), + ), + ), + ); + } +} + +extension on BuildContext { + Color get sidebarUpgradeButtonBackground => Theme.of(this).isLightMode + ? const Color(0xB2EBE4FF) + : const Color(0xB239275B); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 6906dc8b73..d98c24287e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_notification.dart' import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/loading.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; @@ -23,6 +24,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; @@ -34,6 +36,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:auto_updater/auto_updater.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -261,6 +264,9 @@ class _SidebarState extends State<_Sidebar> { final _isHovered = ValueNotifier(false); final _scrollOffset = ValueNotifier(0); + // mute the update button during the current application lifecycle. + final _muteUpdateButton = ValueNotifier(false); + @override void initState() { super.initState(); @@ -347,6 +353,7 @@ class _SidebarState extends State<_Sidebar> { const VSpace(8), _renderUpgradeSpaceButton(menuHorizontalInset), + _buildUpgradeApplicationButton(menuHorizontalInset), const VSpace(8), Padding( @@ -432,6 +439,42 @@ class _SidebarState extends State<_Sidebar> { ); } + Widget _buildUpgradeApplicationButton(EdgeInsets menuHorizontalInset) { + return ValueListenableBuilder( + valueListenable: _muteUpdateButton, + builder: (_, mute, child) { + if (mute) { + return const SizedBox.shrink(); + } + + return ValueListenableBuilder( + valueListenable: ApplicationInfo.latestVersionNotifier, + builder: (_, latestVersion, child) { + if (!ApplicationInfo.isUpdateAvailable) { + return const SizedBox.shrink(); + } + + return Padding( + padding: menuHorizontalInset + + const EdgeInsets.only( + left: 4.0, + right: 4.0, + ), + child: SidebarUpgradeApplicationButton( + onUpdateButtonTap: () { + autoUpdater.checkForUpdates(); + }, + onCloseButtonTap: () { + _muteUpdateButton.value = true; + }, + ), + ); + }, + ); + }, + ); + } + void _onScrollChanged() { setState(() => _isScrolling = true); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index 9b279cc11a..d06016dfb8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -275,6 +275,8 @@ class ConfirmPopup extends StatefulWidget { this.confirmButtonColor, this.child, this.closeOnAction = true, + this.showCloseButton = true, + this.enableKeyboardListener = true, }); final String title; @@ -303,6 +305,16 @@ class ConfirmPopup extends StatefulWidget { /// final bool closeOnAction; + /// Show close button. + /// Defaults to true. + /// + final bool showCloseButton; + + /// Enable keyboard listener. + /// Defaults to true. + /// + final bool enableKeyboardListener; + @override State createState() => _ConfirmPopupState(); } @@ -316,14 +328,16 @@ class _ConfirmPopupState extends State { focusNode: focusNode, autofocus: true, onKeyEvent: (event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.of(context).pop(); - } else if (event is KeyUpEvent && - event.logicalKey == LogicalKeyboardKey.enter) { - widget.onConfirm(); - if (widget.closeOnAction) { + if (widget.enableKeyboardListener) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { Navigator.of(context).pop(); + } else if (event is KeyUpEvent && + event.logicalKey == LogicalKeyboardKey.enter) { + widget.onConfirm(); + if (widget.closeOnAction) { + Navigator.of(context).pop(); + } } } }, @@ -367,15 +381,17 @@ class _ConfirmPopupState extends State { ), ), const HSpace(6.0), - FlowyButton( - margin: const EdgeInsets.all(3), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.upgrade_close_s, - size: Size.square(18.0), + if (widget.showCloseButton) ...[ + FlowyButton( + margin: const EdgeInsets.all(3), + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.upgrade_close_s, + size: Size.square(18.0), + ), + onTap: () => Navigator.of(context).pop(), ), - onTap: () => Navigator.of(context).pop(), - ), + ], ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart new file mode 100644 index 0000000000..85e3c897f6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart @@ -0,0 +1,147 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:auto_updater/auto_updater.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class SettingsAppVersion extends StatelessWidget { + const SettingsAppVersion({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ApplicationInfo.isUpdateAvailable + ? const _UpdateAppSection() + : _buildIsUpToDate(); + } + + Widget _buildIsUpToDate() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const FlowyText.regular( + 'AppFlowy is up to date!', + figmaLineHeight: 17, + ), + const VSpace(4), + Opacity( + opacity: 0.7, + child: FlowyText.regular( + 'Version ${ApplicationInfo.applicationVersion} (Official build)', + fontSize: 12, + figmaLineHeight: 13, + ), + ), + ], + ); + } +} + +class _UpdateAppSection extends StatelessWidget { + const _UpdateAppSection(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded(child: _buildDescription(context)), + _buildUpdateButton(), + ], + ); + } + + Widget _buildUpdateButton() { + return PrimaryRoundedButton( + text: LocaleKeys.autoUpdate_settingsUpdateButton.tr(), + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + fontWeight: FontWeight.w500, + radius: 8.0, + onTap: () { + Log.info('[AutoUpdater] Checking for updates'); + autoUpdater.checkForUpdates(); + }, + ); + } + + Widget _buildDescription(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _buildRedDot(), + const HSpace(6), + Flexible( + child: FlowyText.medium( + LocaleKeys.autoUpdate_settingsUpdateTitle.tr( + namedArgs: { + 'newVersion': ApplicationInfo.latestVersion, + }, + ), + figmaLineHeight: 17, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const VSpace(4), + _buildCurrentVersionAndLatestVersion(context), + ], + ); + } + + Widget _buildCurrentVersionAndLatestVersion(BuildContext context) { + return Row( + children: [ + Flexible( + child: Opacity( + opacity: 0.7, + child: FlowyText.regular( + LocaleKeys.autoUpdate_settingsUpdateDescription.tr( + namedArgs: { + 'currentVersion': ApplicationInfo.applicationVersion, + 'newVersion': ApplicationInfo.latestVersion, + }, + ), + fontSize: 12, + figmaLineHeight: 13, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const HSpace(6), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + afLaunchUrlString('https://www.appflowy.io/what-is-new'); + }, + child: FlowyText.regular( + LocaleKeys.autoUpdate_settingsUpdateWhatsNew.tr(), + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + figmaLineHeight: 13, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } + + Widget _buildRedDot() { + return Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFFFB006D), + shape: BoxShape.circle, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 8d15c80181..13f5d832d0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -14,6 +14,34 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +class AccountSignInOutSection extends StatelessWidget { + const AccountSignInOutSection({ + super.key, + required this.userProfile, + required this.onAction, + this.signIn = true, + }); + + final UserProfilePB userProfile; + final VoidCallback onAction; + final bool signIn; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowyText.regular(LocaleKeys.settings_accountPage_login_title.tr()), + const Spacer(), + AccountSignInOutButton( + userProfile: userProfile, + onAction: onAction, + signIn: signIn, + ), + ], + ); + } +} + class AccountSignInOutButton extends StatelessWidget { const AccountSignInOutButton({ super.key, @@ -32,9 +60,9 @@ class AccountSignInOutButton extends StatelessWidget { text: signIn ? LocaleKeys.settings_accountPage_login_loginLabel.tr() : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - fontWeight: FontWeight.w600, - radius: 12.0, + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + fontWeight: FontWeight.w500, + radius: 8.0, onTap: () => signIn ? _showSignInDialog(context) : _showLogoutDialog(context), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index bb0e4aac9f..701d1cb565 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -2,6 +2,7 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart'; import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; @@ -74,23 +75,42 @@ class _SettingsAccountViewState extends State { title: LocaleKeys.settings_accountPage_email_title.tr(), children: [ FlowyText.regular(state.userProfile.email), + AccountSignInOutSection( + userProfile: state.userProfile, + onAction: state.userProfile.authenticator == + AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, + ), ], ), ], - // user sign in/out + if (isAuthEnabled && + state.userProfile.authenticator == AuthenticatorPB.Local) ...[ + SettingsCategory( + title: LocaleKeys.settings_accountPage_login_title.tr(), + children: [ + AccountSignInOutSection( + userProfile: state.userProfile, + onAction: state.userProfile.authenticator == + AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, + ), + ], + ), + ], + + // App version SettingsCategory( - title: LocaleKeys.settings_accountPage_login_title.tr(), - children: [ - AccountSignInOutButton( - userProfile: state.userProfile, - onAction: - state.userProfile.authenticator == AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.authenticator == - AuthenticatorPB.Local, - ), + title: LocaleKeys.newSettings_myAccount_aboutAppFlowy.tr(), + children: const [ + SettingsAppVersion(), ], ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 1a36ea2a66..aa541b902c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -673,9 +673,13 @@ Future showCustomConfirmDialog({ String? confirmLabel, ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk, bool closeOnConfirm = true, + bool showCloseButton = true, + bool enableKeyboardListener = true, + bool barrierDismissible = true, }) { return showDialog( context: context, + barrierDismissible: barrierDismissible, builder: (context) { return Dialog( shape: RoundedRectangleBorder( @@ -692,6 +696,8 @@ Future showCustomConfirmDialog({ confirmButtonColor: Theme.of(context).colorScheme.primary, style: style, closeOnAction: closeOnConfirm, + showCloseButton: showCloseButton, + enableKeyboardListener: enableKeyboardListener, child: builder(context), ), ), diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 9c0ef58429..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -3,6 +3,9 @@ PODS: - FlutterMacOS - appflowy_backend (0.0.1): - FlutterMacOS + - auto_updater_macos (0.0.1): + - FlutterMacOS + - Sparkle - bitsdojo_window_macos (0.0.1): - FlutterMacOS - connectivity_plus (0.0.1): @@ -17,7 +20,7 @@ PODS: - flowy_infra_ui (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - HotKey (0.2.0) + - HotKey (0.2.1) - hotkey_manager (0.0.1): - FlutterMacOS - HotKey @@ -30,7 +33,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.2.3) + - ReachabilitySwift (5.2.4) - screen_retriever_macos (0.0.1): - FlutterMacOS - Sentry/HybridSDK (8.35.1) @@ -43,6 +46,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - Sparkle (2.6.4) - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -59,6 +63,7 @@ PODS: DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - appflowy_backend (from `Flutter/ephemeral/.symlinks/plugins/appflowy_backend/macos`) + - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) @@ -86,12 +91,15 @@ SPEC REPOS: - HotKey - ReachabilitySwift - Sentry + - Sparkle EXTERNAL SOURCES: app_links: :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos appflowy_backend: :path: Flutter/ephemeral/.symlinks/plugins/appflowy_backend/macos + auto_updater_macos: + :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos bitsdojo_window_macos: :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos connectivity_plus: @@ -136,32 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift index 14f99e9e07..c7872aaec9 100644 --- a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift @@ -16,4 +16,8 @@ class AppDelegate: FlutterAppDelegate { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements b/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements index 71949adefe..4c829c7ab0 100644 --- a/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements @@ -1,20 +1,20 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.temporary-exception.files.absolute-path.read-write - - / - - - + + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/macos/Runner/Info.plist b/frontend/appflowy_flutter/macos/Runner/Info.plist index cd07887134..cb3d1127a0 100644 --- a/frontend/appflowy_flutter/macos/Runner/Info.plist +++ b/frontend/appflowy_flutter/macos/Runner/Info.plist @@ -1,57 +1,61 @@ - - LSApplicationCategoryType - public.app-category.productivity - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - fr - it - zh - - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - appflowy-flutter - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSAppTransportSecurity - NSAllowsArbitraryLoads - + LSApplicationCategoryType + public.app-category.productivity + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + fr + it + zh + + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleURLTypes + + + CFBundleURLName + + CFBundleURLSchemes + + appflowy-flutter + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + SUPublicEDKey + Bs++IOmOwYmNTjMMC2jMqLNldP+mndDp/LwujCg2/kw= + SUAllowsAutomaticUpdates + - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - + \ No newline at end of file diff --git a/frontend/appflowy_flutter/macos/Runner/Release.entitlements b/frontend/appflowy_flutter/macos/Runner/Release.entitlements index 71949adefe..4c829c7ab0 100644 --- a/frontend/appflowy_flutter/macos/Runner/Release.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/Release.entitlements @@ -1,20 +1,20 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.temporary-exception.files.absolute-path.read-write - - / - - - + + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile index 8c77835677..012a925033 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.13' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj index ac3debdf8e..b6c5559634 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -26,11 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49D7864808B727FDFB82A4C2 /* Pods_Runner.framework */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -71,7 +65,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -80,7 +73,6 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; A82DF8E6F43DF0AD4D0653DC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, B7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -145,8 +135,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -215,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -268,6 +256,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -281,7 +270,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -414,7 +403,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -497,7 +486,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -544,7 +533,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3d8205cf56..758981e665 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ab5e606eb2..9b4d235d47 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2511,6 +2511,7 @@ "accountLogin": "Account Login", "updateNameError": "Failed to update name", "updateIconError": "Failed to update icon", + "aboutAppFlowy": "About @:appName", "deleteAccount": { "title": "Delete Account", "subtitle": "Permanently delete your account and all of your data.", @@ -3081,5 +3082,17 @@ }, "ai": { "contentPolicyViolation": "Image generation failed due to sensitive content. Please rephrase your input and try again" + }, + "autoUpdate": { + "criticalUpdateTitle": "Update required to continue", + "criticalUpdateDescription": "We've made improvements to enhance your experience! Please update from {currentVersion} to {newVersion} to keep using the app.", + "criticalUpdateButton": "Update now", + "bannerUpdateTitle": "New Version Available!", + "bannerUpdateDescription": "Get the latest features and bug fixes. Click \"Update\" to install now", + "bannerUpdateButton": "Update", + "settingsUpdateTitle": "New Version ({newVersion}) Available!", + "settingsUpdateDescription": "Current version: {currentVersion} (Official build) → {newVersion}", + "settingsUpdateButton": "Update now", + "settingsUpdateWhatsNew": "What's new" } } From 8f646a2843fb6bfc512a706105a84cb74a6db1b3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 10 Feb 2025 15:07:53 +0800 Subject: [PATCH 020/384] feat: integrate version checker for Linux (#7346) * feat: integrate auto_updater in macOS * chore: update translations * chore: bump auto_updater version * feat: exclude linux platform in auto update task * feat: support auto_updater on Linux * chore: combine version checker and auto updater into same class --- .../version_checker/version_checker.dart | 89 +++++++++++++++++++ .../lib/startup/tasks/auto_update_task.dart | 66 ++++++++------ .../home/menu/sidebar/sidebar.dart | 4 +- .../settings/pages/about/app_version.dart | 14 +-- frontend/appflowy_flutter/pubspec.lock | 2 +- frontend/appflowy_flutter/pubspec.yaml | 1 + frontend/resources/translations/en.json | 4 +- 7 files changed, 145 insertions(+), 35 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart diff --git a/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart b/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart new file mode 100644 index 0000000000..b9fecb2b94 --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart @@ -0,0 +1,89 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:auto_updater/auto_updater.dart'; +import 'package:collection/collection.dart'; +import 'package:http/http.dart' as http; +import 'package:universal_platform/universal_platform.dart'; +import 'package:xml/xml.dart' as xml; + +final versionChecker = VersionChecker(); + +/// Version checker class to handle update checks using appcast XML feeds +class VersionChecker { + factory VersionChecker() => _instance; + + VersionChecker._internal(); + String? _feedUrl; + + static final VersionChecker _instance = VersionChecker._internal(); + + /// Sets the appcast XML feed URL + void setFeedUrl(String url) { + _feedUrl = url; + + if (UniversalPlatform.isWindows || UniversalPlatform.isMacOS) { + autoUpdater.setFeedURL(url); + } + } + + /// Checks for updates by fetching and parsing the appcast XML + /// Returns a list of [AppcastItem] or throws an exception if the feed URL is not set + Future checkForUpdateInformation() async { + if (_feedUrl == null) { + throw Exception('Feed URL not set. Call setFeedUrl() first.'); + } + + try { + final response = await http.get(Uri.parse(_feedUrl!)); + if (response.statusCode != 200) { + throw Exception('Failed to fetch appcast feed'); + } + + // Parse XML content + final document = xml.XmlDocument.parse(response.body); + final items = document.findAllElements('item'); + + // Convert XML items to AppcastItem objects + return items + .map(_parseAppcastItem) + .nonNulls + .firstWhereOrNull((e) => e.os == ApplicationInfo.os); + } catch (e) { + throw Exception('Error checking for updates: $e'); + } + } + + /// For Windows and macOS, calling this API will trigger the auto updater to check for updates + /// For Linux, it will open the official website in the browser if there is a new version + + Future checkForUpdate() async { + if (UniversalPlatform.isLinux) { + // open the official website in the browser + await afLaunchUrlString('https://appflowy.com/download'); + } else { + await autoUpdater.checkForUpdates(); + } + } + + AppcastItem? _parseAppcastItem(xml.XmlElement item) { + final enclosure = item.findElements('enclosure').firstOrNull; + return AppcastItem.fromJson({ + 'title': item.findElements('title').firstOrNull?.innerText, + 'versionString': item + .findElements('sparkle:shortVersionString') + .firstOrNull + ?.innerText, + 'displayVersionString': item + .findElements('sparkle:shortVersionString') + .firstOrNull + ?.innerText, + 'releaseNotesUrl': + item.findElements('releaseNotesLink').firstOrNull?.innerText, + 'pubDate': item.findElements('pubDate').firstOrNull?.innerText, + 'fileURL': enclosure?.getAttribute('url') ?? '', + 'os': enclosure?.getAttribute('sparkle:os') ?? '', + 'criticalUpdate': + enclosure?.getAttribute('sparkle:criticalUpdate') ?? false, + }); + } +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart index 3371abf224..5846b31bef 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -14,37 +15,22 @@ import '../startup.dart'; class AutoUpdateTask extends LaunchTask { AutoUpdateTask(); - // static const _feedUrl = - // 'https://github.com/LucasXu0/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml'; + static const _feedUrl = + 'https://github.com/LucasXu0/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml'; final _listener = _AppFlowyAutoUpdaterListener(); @override Future initialize(LaunchContext context) async { - // Enable auto update when the integration of Windows and Linux is completed. - return; - // // the auto updater is not supported on mobile and linux - // if (UniversalPlatform.isMobile || UniversalPlatform.isLinux) { - // return; - // } + // the auto updater is not supported on mobile + if (UniversalPlatform.isMobile) { + return; + } - // Log.info( - // '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}', - // ); + await _setupAutoUpdater(); - // autoUpdater.addListener(_listener); - - // // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch. - // final feedUrl = _feedUrl - // .replaceAll('{os}', ApplicationInfo.os) - // .replaceAll('{arch}', ApplicationInfo.architecture); - // Log.info('[AutoUpdate] feed url: $feedUrl'); - - // await autoUpdater.setFeedURL(feedUrl); - // await autoUpdater.checkForUpdateInformation(); - - // ApplicationInfo.isCriticalUpdateNotifier.addListener( - // _showCriticalUpdateDialog, - // ); + ApplicationInfo.isCriticalUpdateNotifier.addListener( + _showCriticalUpdateDialog, + ); } @override @@ -56,6 +42,34 @@ class AutoUpdateTask extends LaunchTask { ); } + // On macOS and windows, we use auto_updater to check for updates. + // On linux, we use the version checker to check for updates because the auto_updater is not supported. + Future _setupAutoUpdater() async { + Log.info( + '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}', + ); + + // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch. + final feedUrl = _feedUrl + .replaceAll('{os}', ApplicationInfo.os) + .replaceAll('{arch}', ApplicationInfo.architecture); + + // the auto updater is only supported on macOS and windows, so we don't need to check the platform + if (UniversalPlatform.isMacOS || UniversalPlatform.isWindows) { + autoUpdater.addListener(_listener); + } + + Log.info('[AutoUpdate] feed url: $feedUrl'); + + versionChecker.setFeedUrl(feedUrl); + final item = await versionChecker.checkForUpdateInformation(); + if (item != null) { + ApplicationInfo.latestAppcastItem = item; + ApplicationInfo.latestVersionNotifier.value = + item.displayVersionString ?? ''; + } + } + void _showCriticalUpdateDialog() { showCustomConfirmDialog( context: AppGlobals.rootNavKey.currentContext!, @@ -74,7 +88,7 @@ class AutoUpdateTask extends LaunchTask { closeOnConfirm: false, confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(), onConfirm: () async { - await autoUpdater.checkForUpdates(); + await versionChecker.checkForUpdate(); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index d98c24287e..ea55c72f16 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/loading.dart'; +import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; @@ -36,7 +37,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:auto_updater/auto_updater.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -462,7 +462,7 @@ class _SidebarState extends State<_Sidebar> { ), child: SidebarUpgradeApplicationButton( onUpdateButtonTap: () { - autoUpdater.checkForUpdates(); + versionChecker.checkForUpdate(); }, onCloseButtonTap: () { _muteUpdateButton.value = true; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart index 85e3c897f6..71dfdde7a9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart @@ -1,8 +1,8 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:auto_updater/auto_updater.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -23,15 +23,19 @@ class SettingsAppVersion extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const FlowyText.regular( - 'AppFlowy is up to date!', + FlowyText.regular( + LocaleKeys.settings_accountPage_isUpToDate.tr(), figmaLineHeight: 17, ), const VSpace(4), Opacity( opacity: 0.7, child: FlowyText.regular( - 'Version ${ApplicationInfo.applicationVersion} (Official build)', + LocaleKeys.settings_accountPage_officialVersion.tr( + namedArgs: { + 'version': ApplicationInfo.applicationVersion, + }, + ), fontSize: 12, figmaLineHeight: 13, ), @@ -62,7 +66,7 @@ class _UpdateAppSection extends StatelessWidget { radius: 8.0, onTap: () { Log.info('[AutoUpdater] Checking for updates'); - autoUpdater.checkForUpdates(); + versionChecker.checkForUpdate(); }, ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 13771d85db..3c5ca468af 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -2540,7 +2540,7 @@ packages: source: hosted version: "1.1.0" xml: - dependency: transitive + dependency: "direct main" description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 21d0b942c7..5643e3b09b 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: # Window Manager for MacOS and Linux version: ^3.0.2 + xml: ^6.5.0 window_manager: ^0.4.3 dev_dependencies: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9b4d235d47..98c0167327 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -571,7 +571,9 @@ "title": "Account login", "loginLabel": "Log in", "logoutLabel": "Log out" - } + }, + "isUpToDate": "@:appName is up to date!", + "officialVersion": "Version {version} (Official build)" }, "workspacePage": { "menuLabel": "Workspace", From 12d9a988319e28795a9fc496b94f671401863a3e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:03:29 +0800 Subject: [PATCH 021/384] chore: disable impeller on android (#7355) --- .../appflowy_flutter/android/app/src/main/AndroidManifest.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 74d5c5e494..fe9744ef8f 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -43,6 +43,8 @@ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> + @@ -66,4 +68,4 @@ - \ No newline at end of file + From 5d73c3d19459bdd18970c38889cf3ba548e6cc4f Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 11 Feb 2025 14:03:49 +0800 Subject: [PATCH 022/384] fix: gallery not rendering in row page (#7349) --- .../layouts/image_browser_layout.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart index c0f91a27f0..eb8ddba0b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart @@ -27,7 +27,7 @@ import 'package:provider/provider.dart'; import '../image_render.dart'; -const _thumbnailItemSize = 100.0; +const _thumbnailItemSize = 100.0, _imageHeight = 400.0; class ImageBrowserLayout extends ImageBlockMultiLayout { const ImageBrowserLayout({ @@ -59,13 +59,13 @@ class _ImageBrowserLayoutState extends State { @override Widget build(BuildContext context) { - return Stack( + final gallery = Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: 400, + height: _imageHeight, width: MediaQuery.of(context).size.width, child: GestureDetector( onDoubleTap: () => _openInteractiveViewer(context), @@ -258,6 +258,10 @@ class _ImageBrowserLayoutState extends State { ), ], ); + return SizedBox( + height: _imageHeight + _thumbnailItemSize + 20, + child: gallery, + ); } void _openInteractiveViewer(BuildContext context, [int? index]) => showDialog( From 04e3246976d40e787b2927925940c9d52a71f7d0 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:40:39 +0800 Subject: [PATCH 023/384] chore: rename predefined format enum variant (#7359) --- .../prompt_input/predefined_format_buttons.dart | 5 +++-- .../lib/plugins/ai_chat/application/chat_entity.dart | 10 +++++----- .../message/ai_change_format_bottom_sheet.dart | 5 +++-- .../presentation/message/ai_message_action_bar.dart | 5 +++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart index fe3b07cc13..13db6a0669 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -69,7 +69,7 @@ class ChangeFormatBar extends StatelessWidget { _buildFormatButton(context, ImageFormat.image), if (predefinedFormat?.imageFormat.hasText ?? true) ...[ _buildDivider(), - _buildTextFormatButton(context, TextFormat.auto), + _buildTextFormatButton(context, TextFormat.paragraph), _buildTextFormatButton(context, TextFormat.bulletList), _buildTextFormatButton(context, TextFormat.numberedList), _buildTextFormatButton(context, TextFormat.table), @@ -88,7 +88,8 @@ class ChangeFormatBar extends StatelessWidget { return; } if (format.hasText) { - final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto; + final textFormat = + predefinedFormat?.textFormat ?? TextFormat.paragraph; onSelectPredefinedFormat( PredefinedFormat(imageFormat: format, textFormat: textFormat), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart index fe2ed44193..7b16c597cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart @@ -149,7 +149,7 @@ class PredefinedFormat extends Equatable { const PredefinedFormat.auto() : imageFormat = ImageFormat.text, - textFormat = TextFormat.auto; + textFormat = TextFormat.paragraph; final ImageFormat imageFormat; final TextFormat? textFormat; @@ -162,7 +162,7 @@ class PredefinedFormat extends Equatable { ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage, }, textFormat: switch (textFormat) { - TextFormat.auto => ResponseTextFormatPB.Paragraph, + TextFormat.paragraph => ResponseTextFormatPB.Paragraph, TextFormat.bulletList => ResponseTextFormatPB.BulletedList, TextFormat.numberedList => ResponseTextFormatPB.NumberedList, TextFormat.table => ResponseTextFormatPB.Table, @@ -201,14 +201,14 @@ enum ImageFormat { } enum TextFormat { - auto, + paragraph, bulletList, numberedList, table; FlowySvgData get icon { return switch (this) { - TextFormat.auto => FlowySvgs.ai_paragraph_s, + TextFormat.paragraph => FlowySvgs.ai_paragraph_s, TextFormat.bulletList => FlowySvgs.ai_list_s, TextFormat.numberedList => FlowySvgs.ai_number_list_s, TextFormat.table => FlowySvgs.ai_table_s, @@ -217,7 +217,7 @@ enum TextFormat { String get i18n { return switch (this) { - TextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(), + TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(), TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(), TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(), TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart index 7edc5f7d5e..f68453df75 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart @@ -122,7 +122,7 @@ class _Body extends StatelessWidget { opacity: predefinedFormat?.imageFormat.hasText ?? true ? 1 : 0, child: Column( children: [ - _buildTextFormatButton(TextFormat.auto, true), + _buildTextFormatButton(TextFormat.paragraph, true), _buildTextFormatButton(TextFormat.bulletList), _buildTextFormatButton(TextFormat.numberedList), _buildTextFormatButton(TextFormat.table), @@ -153,7 +153,8 @@ class _Body extends StatelessWidget { return; } if (format.hasText) { - final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto; + final textFormat = + predefinedFormat?.textFormat ?? TextFormat.paragraph; onSelectPredefinedFormat( PredefinedFormat(imageFormat: format, textFormat: textFormat), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 1dbeeb0ec2..bb79a6b2d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -377,8 +377,9 @@ class _ChangeFormatPopoverContentState child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - widget.onRegenerate - ?.call(predefinedFormat ?? const PredefinedFormat.auto()); + if (predefinedFormat != null) { + widget.onRegenerate?.call(predefinedFormat!); + } }, child: SizedBox.square( dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight, From 552dba5abea019c7d27986f0f7d18cf68002942c Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 11 Feb 2025 21:46:02 +0800 Subject: [PATCH 024/384] fix: support exporting more content to markdown (#7333) * fix: support exporting to markdown with multiple images * fix: support exporting to markdown with database * fix: support exporting to markdown with date or reminder * fix: support exporting to markdown with subpage and page reference * chore: add some testing for markdown parser * chore: add testing for exporting markdown with databse as csv --- .../uncategorized/share_markdown_test.dart | 58 +++++++++++++--- .../ai/ask_ai_toolbar_item.dart | 4 +- .../ai/util/ask_ai_node_extension.dart | 4 +- .../multi_image_block_component.dart | 5 +- .../parsers/custom_image_node_parser.dart | 66 +++++++++++++++++++ .../parsers/custom_paragraph_node_parser.dart | 37 +++++++++++ .../parsers/database_node_parser.dart | 53 +++++++++++++++ .../parsers/document_markdown_parsers.dart | 2 + .../parsers/sub_page_node_parser.dart | 20 ++++++ .../lib/plugins/shared/share/export_tab.dart | 2 +- .../lib/plugins/shared/share/share_bloc.dart | 14 +++- .../lib/shared/markdown_to_document.dart | 63 +++++++++++++++++- .../application/export/document_exporter.dart | 17 +++-- .../markdown/markdown_parser_test.dart | 66 ++++++++++++++++++- 14 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index 9a4fe30815..8c3c29ab77 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -1,12 +1,16 @@ +import 'dart:convert'; import 'dart:io'; import 'package:appflowy/plugins/shared/share/share_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; +import 'package:archive/archive.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; +import '../document/document_with_database_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -18,7 +22,7 @@ void main() { // mock the file picker final path = await mockSaveFilePath( - p.join(context.applicationDataDirectory, 'test.md'), + p.join(context.applicationDataDirectory, 'test.zip'), ); // click the share button and select markdown await tester.tapShareButton(); @@ -28,10 +32,14 @@ void main() { tester.expectToExportSuccess(); final file = File(path); - final isExist = file.existsSync(); - expect(isExist, true); - final markdown = file.readAsStringSync(); - expect(markdown, expectedMarkdown); + expect(file.existsSync(), true); + final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); + for (final entry in archive) { + if (entry.isFile && entry.name.endsWith('.md')) { + final markdown = utf8.decode(entry.content); + expect(markdown, expectedMarkdown); + } + } }); testWidgets( @@ -57,7 +65,7 @@ void main() { final path = await mockSaveFilePath( p.join( context.applicationDataDirectory, - '${shareButtonState.view.name}.md', + '${shareButtonState.view.name}.zip', ), ); @@ -69,10 +77,44 @@ void main() { tester.expectToExportSuccess(); final file = File(path); - final isExist = file.existsSync(); - expect(isExist, true); + expect(file.existsSync(), true); + final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); + for (final entry in archive) { + if (entry.isFile && entry.name.endsWith('.md')) { + final markdown = utf8.decode(entry.content); + expect(markdown, expectedMarkdown); + } + } }, ); + + testWidgets('share the markdown with database', (tester) async { + final context = await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await insertLinkedDatabase(tester, ViewLayoutPB.Grid); + + // mock the file picker + final path = await mockSaveFilePath( + p.join(context.applicationDataDirectory, 'test.zip'), + ); + // click the share button and select markdown + await tester.tapShareButton(); + await tester.tapMarkdownButton(); + + // expect to see the success dialog + tester.expectToExportSuccess(); + + final file = File(path); + expect(file.existsSync(), true); + final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); + bool hasCsvFile = false; + for (final entry in archive) { + if (entry.isFile && entry.name.endsWith('.csv')) { + hasCsvFile = true; + } + } + expect(hasCsvFile, true); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart index 723b7f4ffb..9b413e1270 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart @@ -11,8 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'widgets/ask_ai_action.dart'; import 'ask_ai_block_component.dart'; +import 'widgets/ask_ai_action.dart'; const _kAskAIToolbarItemId = 'appflowy.editor.ask_ai'; @@ -118,7 +118,7 @@ class _AskAIActionListState extends State { return; } - final markdown = editorState.getMarkdownInSelection(selection); + final markdown = await editorState.getMarkdownInSelection(selection); final transaction = editorState.transaction; transaction.insertNode( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart index 933712217f..9ed5b046b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart @@ -2,7 +2,7 @@ import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; extension AskAINodeExtension on EditorState { - String getMarkdownInSelection(Selection? selection) { + Future getMarkdownInSelection(Selection? selection) async { selection ??= this.selection?.normalized; if (selection == null || selection.isCollapsed) { return ''; @@ -33,7 +33,7 @@ extension AskAINodeExtension on EditorState { slicedNodes.add(copiedNode); } - final markdown = customDocumentToMarkdown( + final markdown = await customDocumentToMarkdown( Document.blank()..insert([0], slicedNodes), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart index 272b492835..88f60db494 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart @@ -13,10 +13,11 @@ import 'package:universal_platform/universal_platform.dart'; const kMultiImagePlaceholderKey = 'multiImagePlaceholderKey'; -Node multiImageNode() => Node( +Node multiImageNode({List? images}) => Node( type: MultiImageBlockKeys.type, attributes: { - MultiImageBlockKeys.images: MultiImageData(images: []).toJson(), + MultiImageBlockKeys.images: + MultiImageData(images: images ?? []).toJson(), MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(), }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart index 91398302ed..d4b6bb444f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart @@ -1,4 +1,9 @@ +import 'dart:io'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:archive/archive.dart'; +import 'package:path/path.dart' as p; import '../image/custom_image_block_component/custom_image_block_component.dart'; @@ -16,3 +21,64 @@ class CustomImageNodeParser extends NodeParser { return '![]($url)\n'; } } + +class CustomImageNodeFileParser extends NodeParser { + const CustomImageNodeFileParser(this.files, this.dirPath); + + final List> files; + final String dirPath; + + @override + String get id => ImageBlockKeys.type; + + @override + String transform(Node node, DocumentMarkdownEncoder? encoder) { + assert(node.children.isEmpty); + final url = node.attributes[CustomImageBlockKeys.url]; + final hasFile = File(url).existsSync(); + if (hasFile) { + final bytes = File(url).readAsBytesSync(); + files.add( + Future.value( + ArchiveFile(p.join(dirPath, p.basename(url)), bytes.length, bytes), + ), + ); + return '![](${p.join(dirPath, p.basename(url))})\n'; + } + assert(url != null); + return '![]($url)\n'; + } +} + +class CustomMultiImageNodeFileParser extends NodeParser { + const CustomMultiImageNodeFileParser(this.files, this.dirPath); + + final List> files; + final String dirPath; + + @override + String get id => MultiImageBlockKeys.type; + + @override + String transform(Node node, DocumentMarkdownEncoder? encoder) { + assert(node.children.isEmpty); + final images = node.attributes[MultiImageBlockKeys.images] as List; + final List markdownImages = []; + for (final image in images) { + final String url = image['url'] ?? ''; + if (url.isEmpty) continue; + final hasFile = File(url).existsSync(); + if (hasFile) { + final bytes = File(url).readAsBytesSync(); + final filePath = p.join(dirPath, p.basename(url)); + files.add( + Future.value(ArchiveFile(filePath, bytes.length, bytes)), + ); + markdownImages.add('![]($filePath)'); + } else { + markdownImages.add('![]($url)'); + } + } + return markdownImages.join('\n'); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart new file mode 100644 index 0000000000..b7d7674137 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class CustomParagraphNodeParser extends NodeParser { + const CustomParagraphNodeParser(); + + @override + String get id => ParagraphBlockKeys.type; + + @override + String transform(Node node, DocumentMarkdownEncoder? encoder) { + final delta = node.delta; + if (delta != null) { + for (final o in delta) { + final attribute = o.attributes ?? {}; + final Map? mention = attribute[MentionBlockKeys.mention] ?? {}; + if (mention == null) continue; + + /// filter date reminder node, and return it + final String date = mention[MentionBlockKeys.date] ?? ''; + if (date.isNotEmpty) { + final dateTime = DateTime.tryParse(date); + if (dateTime == null) continue; + return '${DateFormat.yMMMd().format(dateTime)}\n'; + } + + /// filter reference page + final String pageId = mention[MentionBlockKeys.pageId] ?? ''; + if (pageId.isNotEmpty) { + return '[]($pageId)\n'; + } + } + } + return const TextNodeParser().transform(node, encoder); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart new file mode 100644 index 0000000000..3ba599d491 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart @@ -0,0 +1,53 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; +import 'package:appflowy/workspace/application/settings/share/export_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:archive/archive.dart'; +import 'package:path/path.dart' as p; + +abstract class DatabaseNodeParser extends NodeParser { + DatabaseNodeParser(this.files, this.dirPath); + + final List> files; + final String dirPath; + + @override + String transform(Node node, DocumentMarkdownEncoder? encoder) { + final String viewId = node.attributes[DatabaseBlockKeys.viewID] ?? ''; + if (viewId.isEmpty) return ''; + files.add(_convertDatabaseToCSV(viewId)); + return '[](${p.join(dirPath, '$viewId.csv')})\n'; + } + + Future _convertDatabaseToCSV(String viewId) async { + final result = await BackendExportService.exportDatabaseAsCSV(viewId); + final filePath = p.join(dirPath, '$viewId.csv'); + ArchiveFile file = ArchiveFile.string(filePath, ''); + result.fold( + (s) => file = ArchiveFile.string(filePath, s.data), + (f) => Log.error('convertDatabaseToCSV error with $viewId, error: $f'), + ); + return file; + } +} + +class GridNodeParser extends DatabaseNodeParser { + GridNodeParser(super.files, super.dirPath); + + @override + String get id => DatabaseBlockKeys.gridType; +} + +class BoardNodeParser extends DatabaseNodeParser { + BoardNodeParser(super.files, super.dirPath); + + @override + String get id => DatabaseBlockKeys.boardType; +} + +class CalendarNodeParser extends DatabaseNodeParser { + CalendarNodeParser(super.files, super.dirPath); + + @override + String get id => DatabaseBlockKeys.calendarType; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart index 3f2895d57e..c0a15629b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart @@ -1,5 +1,7 @@ export 'callout_node_parser.dart'; export 'custom_image_node_parser.dart'; +export 'custom_paragraph_node_parser.dart'; +export 'database_node_parser.dart'; export 'file_block_node_parser.dart'; export 'link_preview_node_parser.dart'; export 'math_equation_node_parser.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart new file mode 100644 index 0000000000..1cf0c569bc --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart @@ -0,0 +1,20 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +class SubPageNodeParser extends NodeParser { + const SubPageNodeParser(); + + @override + String get id => SubPageBlockKeys.type; + + @override + String transform(Node node, DocumentMarkdownEncoder? encoder) { + final String viewId = node.attributes[SubPageBlockKeys.viewId] ?? ''; + if (viewId.isNotEmpty) { + final view = pageMemorizer[viewId]; + return '[$viewId](${view?.name ?? ''})\n'; + } + return ''; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index bf0b7fee26..f788d99eb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -105,7 +105,7 @@ class ExportTab extends StatelessWidget { final viewName = context.read().state.viewName; final exportPath = await getIt().saveFile( dialogTitle: '', - fileName: '${viewName.toFileName()}.md', + fileName: '${viewName.toFileName()}.zip', ); if (context.mounted && exportPath != null) { context.read().add( diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index c42bbda5a0..a852fa5e38 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -324,19 +324,21 @@ class ShareBloc extends Bloc { (f) => FlowyResult.failure(f), ); } else { - result = await documentExporter.export(type.documentExportType); + result = + await documentExporter.export(type.documentExportType, path: path); } return result.fold( (s) { if (path != null) { switch (type) { - case ShareType.markdown: case ShareType.html: case ShareType.csv: case ShareType.json: case ShareType.rawDatabaseData: File(path).writeAsStringSync(s); return FlowyResult.success(type); + case ShareType.markdown: + return FlowyResult.success(type); default: break; } @@ -387,22 +389,30 @@ enum ShareType { @freezed class ShareEvent with _$ShareEvent { const factory ShareEvent.initial() = _Initial; + const factory ShareEvent.share( ShareType type, String? path, ) = _Share; + const factory ShareEvent.publish( String nameSpace, String pageId, List selectedViewIds, ) = _Publish; + const factory ShareEvent.unPublish() = _UnPublish; + const factory ShareEvent.updateViewName(String name, String viewId) = _UpdateViewName; + const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus; + const factory ShareEvent.setPublishStatus(bool isPublished) = _SetPublishStatus; + const factory ShareEvent.updatePathName(String pathName) = _UpdatePathName; + const factory ShareEvent.clearPathNameResult() = _ClearPathNameResult; } diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 5763906b57..5664211828 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -1,5 +1,13 @@ +import 'dart:io'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:archive/archive.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as p; +import 'package:share_plus/share_plus.dart'; Document customMarkdownToDocument( String markdown, { @@ -14,17 +22,66 @@ Document customMarkdownToDocument( ); } -String customDocumentToMarkdown(Document document) { - return documentToMarkdown( +Future customDocumentToMarkdown( + Document document, { + String path = '', + AsyncValueSetter? onArchive, +}) async { + final List> fileFutures = []; + + /// create root Archive and directory + final id = document.root.id, + archive = Archive(), + resourceDir = ArchiveFile('$id/', 0, null)..isFile = false, + fileName = p.basenameWithoutExtension(path), + dirName = resourceDir.name; + + final markdown = documentToMarkdown( document, customParsers: [ const MathEquationNodeParser(), const CalloutNodeParser(), const ToggleListNodeParser(), - const CustomImageNodeParser(), + CustomImageNodeFileParser(fileFutures, dirName), + CustomMultiImageNodeFileParser(fileFutures, dirName), + GridNodeParser(fileFutures, dirName), + BoardNodeParser(fileFutures, dirName), + CalendarNodeParser(fileFutures, dirName), + const CustomParagraphNodeParser(), + const SubPageNodeParser(), const SimpleTableNodeParser(), const LinkPreviewNodeParser(), const FileBlockNodeParser(), ], ); + + /// create resource directory + if (fileFutures.isNotEmpty) archive.addFile(resourceDir); + + /// add markdown file to Archive + archive.addFile(ArchiveFile.string('$fileName-$id.md', markdown)); + + for (final fileFuture in fileFutures) { + archive.addFile(await fileFuture); + } + if (archive.isNotEmpty && path.isNotEmpty) { + if (onArchive == null) { + final zipEncoder = ZipEncoder(); + final zip = zipEncoder.encode(archive); + if (zip != null) { + final zipFile = await File(path).writeAsBytes(zip); + if (Platform.isIOS) { + await Share.shareUri(zipFile.uri); + await zipFile.delete(); + } else if (Platform.isAndroid) { + await Share.shareXFiles([XFile(zipFile.path)]); + await zipFile.delete(); + } + Log.info('documentToMarkdownFiles to $path'); + } + } else { + await onArchive.call(archive); + } + } + return markdown; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart index df46ab97e7..a17b5741bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -25,12 +25,13 @@ class DocumentExporter { final ViewPB view; Future> export( - DocumentExportType type, - ) async { + DocumentExportType type, { + String? path, + }) async { final documentService = DocumentService(); final result = await documentService.openDocument(documentId: view.id); return result.fold( - (r) { + (r) async { final document = r.toDocument(); if (document == null) { return FlowyResult.failure( @@ -43,8 +44,14 @@ class DocumentExporter { case DocumentExportType.json: return FlowyResult.success(jsonEncode(document)); case DocumentExportType.markdown: - final markdown = customDocumentToMarkdown(document); - return FlowyResult.success(markdown); + if (path != null) { + await customDocumentToMarkdown(document, path: path); + return FlowyResult.success(''); + } else { + return FlowyResult.success( + await customDocumentToMarkdown(document), + ); + } case DocumentExportType.text: throw UnimplementedError(); case DocumentExportType.html: diff --git a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart b/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart index 70775612e2..707cc23d4f 100644 --- a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart @@ -1,7 +1,10 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -17,21 +20,78 @@ void main() { ), ], ); - final markdown = customDocumentToMarkdown(document); + final markdown = await customDocumentToMarkdown(document); expect(markdown, '[file.txt](https://file.com)\n'); }); - test('link preview', () { + test('link preview', () async { final document = Document.blank() ..insert( [0], [linkPreviewNode(url: 'https://www.link_preview.com')], ); - final markdown = customDocumentToMarkdown(document); + final markdown = await customDocumentToMarkdown(document); expect( markdown, '[https://www.link_preview.com](https://www.link_preview.com)\n', ); }); + + test('multiple images', () async { + const png1 = 'https://www.appflowy.png', + png2 = 'https://www.appflowy2.png'; + final document = Document.blank() + ..insert( + [0], + [ + multiImageNode( + images: [ + ImageBlockData( + url: png1, + type: CustomImageType.external, + ), + ImageBlockData( + url: png2, + type: CustomImageType.external, + ), + ], + ), + ], + ); + final markdown = await customDocumentToMarkdown(document); + expect( + markdown, + '![]($png1)\n![]($png2)', + ); + }); + + test('subpage block', () async { + const testSubpageId = 'testSubpageId'; + final subpageNode = pageMentionNode(testSubpageId); + final document = Document.blank() + ..insert( + [0], + [subpageNode], + ); + final markdown = await customDocumentToMarkdown(document); + expect( + markdown, + '[]($testSubpageId)\n', + ); + }); + + test('date or reminder', () async { + final dateTime = DateTime.now(); + final document = Document.blank() + ..insert( + [0], + [dateMentionNode()], + ); + final markdown = await customDocumentToMarkdown(document); + expect( + markdown, + '${DateFormat.yMMMd().format(dateTime)}\n', + ); + }); }); } From 71ce9affbe10350494042e63f3c3383616ae12ad Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 12 Feb 2025 09:49:36 +0800 Subject: [PATCH 025/384] feat: lock page (#7353) * feat: lock page * feat: add pageLockStatus bloc * feat: add lock status and unlock status in title bar * feat: add loading lock status * feat: disable moveTo, delete, rename, updateIcon operations if the page is locked * fix: lock toast issue * feat: support locked database * feat: support locked grid * feat: support locked title * feat: support locked board * feat: support locked calendar --- .../board/presentation/board_page.dart | 29 +++- .../calendar/presentation/calendar_page.dart | 68 +++++++--- .../database/grid/presentation/grid_page.dart | 46 ++++++- .../grid/presentation/widgets/row/row.dart | 22 ++- .../database/tab_bar/tab_bar_view.dart | 28 +++- .../lib/plugins/document/document_page.dart | 67 +++++++--- .../document/presentation/editor_page.dart | 5 +- .../workspace/application/view/view_ext.dart | 5 + .../view/view_lock_status_bloc.dart | 123 +++++++++++++++++ .../application/view/view_service.dart | 10 ++ .../home/menu/view/view_action_type.dart | 13 ++ .../home/menu/view/view_item.dart | 11 +- .../menu/view/view_more_action_button.dart | 53 +++++--- .../pages/account/account_deletion.dart | 3 +- .../more_view_actions/more_view_actions.dart | 66 ++++++--- .../widgets/lock_page_action.dart | 119 +++++++++++++++++ .../presentation/widgets/view_title_bar.dart | 126 ++++++++++++++++-- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 4 +- .../resources/flowy_icons/16x/lock_page.svg | 4 + .../resources/flowy_icons/16x/unlock_page.svg | 4 + frontend/resources/translations/en.json | 10 +- frontend/rust-lib/Cargo.lock | 34 ++--- frontend/rust-lib/Cargo.toml | 16 +-- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 2 + .../flowy-folder/src/entities/view.rs | 8 ++ .../flowy-folder/src/event_handler.rs | 22 +++ .../rust-lib/flowy-folder/src/event_map.rs | 8 ++ frontend/rust-lib/flowy-folder/src/manager.rs | 53 +++++++- .../flowy-folder/src/view_operation.rs | 1 + 31 files changed, 812 insertions(+), 155 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart create mode 100644 frontend/resources/flowy_icons/16x/lock_page.svg create mode 100644 frontend/resources/flowy_icons/16x/unlock_page.svg diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index b170ee2fef..412d2bbbd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -17,6 +17,7 @@ import 'package:appflowy/shared/conditional_listenable_builder.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -205,6 +206,7 @@ class _DesktopBoardPageState extends State { onEditStateChanged: widget.onEditStateChanged, focusScope: _focusScope, boardController: _boardController, + view: widget.view, ), ), ), @@ -239,6 +241,7 @@ class _BoardContent extends StatefulWidget { const _BoardContent({ required this.boardController, required this.focusScope, + required this.view, this.onEditStateChanged, this.shrinkWrap = false, }); @@ -247,6 +250,7 @@ class _BoardContent extends StatefulWidget { final BoardFocusScope focusScope; final VoidCallback? onEditStateChanged; final bool shrinkWrap; + final ViewPB view; @override State<_BoardContent> createState() => _BoardContentState(); @@ -366,7 +370,7 @@ class _BoardContentState extends State<_BoardContent> { scrollManager: scrollManager, ), ), - cardBuilder: (_, column, columnItem) => MultiBlocProvider( + cardBuilder: (context, column, columnItem) => MultiBlocProvider( key: ValueKey("board_card_${column.id}_${columnItem.id}"), providers: [ BlocProvider.value( @@ -375,13 +379,24 @@ class _BoardContentState extends State<_BoardContent> { BlocProvider.value( value: context.read(), ), + BlocProvider( + create: (_) => ViewLockStatusBloc(view: widget.view) + ..add(ViewLockStatusEvent.initial()), + ), ], - child: _BoardCard( - afGroupData: column, - groupItem: columnItem as GroupItem, - boardConfig: config, - notifier: widget.focusScope, - cellBuilder: cellBuilder, + child: BlocBuilder( + builder: (context, state) { + return IgnorePointer( + ignoring: state.isLocked, + child: _BoardCard( + afGroupData: column, + groupItem: columnItem as GroupItem, + boardConfig: config, + notifier: widget.focusScope, + cellBuilder: cellBuilder, + ), + ); + }, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 8c46cf75a8..7ecc306e7a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -1,9 +1,3 @@ -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -12,21 +6,26 @@ import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:universal_platform/universal_platform.dart'; import '../../application/row/row_controller.dart'; import '../../widgets/row/row_detail.dart'; - import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; @@ -123,8 +122,18 @@ class _CalendarPageState extends State { Widget build(BuildContext context) { return CalendarControllerProvider( controller: _eventController, - child: BlocProvider.value( - value: _calendarBloc, + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: _calendarBloc, + ), + BlocProvider( + create: (context) => ViewLockStatusBloc(view: widget.view) + ..add( + ViewLockStatusEvent.initial(), + ), + ), + ], child: MultiBlocListener( listeners: [ BlocListener( @@ -235,7 +244,21 @@ class _CalendarPageState extends State { showBorder: false, headerBuilder: _headerNavigatorBuilder, weekDayBuilder: _headerWeekDayBuilder, - cellBuilder: _calendarDayBuilder, + cellBuilder: ( + date, + calenderEvents, + isToday, + isInMonth, + position, + ) => + _calendarDayBuilder( + context, + date, + calenderEvents, + isToday, + isInMonth, + position, + ), useAvailableVerticalSpace: widget.shrinkWrap, ), ), @@ -344,6 +367,7 @@ class _CalendarPageState extends State { } Widget _calendarDayBuilder( + BuildContext context, DateTime date, List> calenderEvents, isToday, @@ -355,17 +379,21 @@ class _CalendarPageState extends State { // is implemnted in the develop branch(WIP). Will be replaced with that. final events = calenderEvents.map((value) => value.event!).toList() ..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp)); + final isLocked = context.watch().state.isLocked; - return CalendarDayCard( - viewId: widget.view.id, - isToday: isToday, - isInMonth: isInMonth, - events: events, - date: date, - rowCache: _calendarBloc.rowCache, - onCreateEvent: (date) => - _calendarBloc.add(CalendarEvent.createEvent(date)), - position: position, + return IgnorePointer( + ignoring: isLocked, + child: CalendarDayCard( + viewId: widget.view.id, + isToday: isToday, + isInMonth: isInMonth, + events: events, + date: date, + rowCache: _calendarBloc.rowCache, + onCreateEvent: (date) => + _calendarBloc.add(CalendarEvent.createEvent(date)), + position: position, + ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 851ed2f7b1..5d3ec15b46 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -12,6 +12,7 @@ import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -138,8 +139,16 @@ class _GridPageState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => gridBloc, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => gridBloc, + ), + BlocProvider( + create: (context) => ViewLockStatusBloc(view: widget.view) + ..add(ViewLockStatusEvent.initial()), + ), + ], child: BlocListener( listener: (context, state) { final action = state.action; @@ -286,7 +295,10 @@ class _GridPageContentState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _GridHeader(headerScrollController: headerScrollController), + _GridHeader( + headerScrollController: headerScrollController, + editable: !context.read().state.isLocked, + ), _GridRows( viewId: widget.view.id, scrollController: _scrollController, @@ -298,18 +310,30 @@ class _GridPageContentState extends State { } class _GridHeader extends StatelessWidget { - const _GridHeader({required this.headerScrollController}); + const _GridHeader({ + required this.headerScrollController, + required this.editable, + }); final ScrollController headerScrollController; + final bool editable; @override Widget build(BuildContext context) { - return BlocBuilder( + Widget child = BlocBuilder( builder: (_, state) => GridHeaderSliverAdaptor( viewId: state.viewId, anchorScrollController: headerScrollController, ), ); + + if (!editable) { + child = IgnorePointer( + child: child, + ); + } + + return child; } } @@ -502,12 +526,21 @@ class _GridRowsState extends State<_GridRows> { itemCount: itemCount, itemBuilder: (context, index) { if (index == itemCount - 1) { - return Column( + final child = Column( key: const Key('grid_footer'), mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: footer, ); + + if (context.read().state.isLocked) { + return IgnorePointer( + key: const Key('grid_footer'), + child: child, + ); + } + + return child; } return _renderRow( @@ -542,6 +575,7 @@ class _GridRowsState extends State<_GridRows> { rowId: rowId, viewId: viewId, index: index, + editable: !context.watch().state.isLocked, rowController: RowController( viewId: viewId, rowMeta: rowMeta, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 4de6f20bc2..dd66f34e4d 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -1,27 +1,25 @@ -import 'package:appflowy/plugins/database/domain/sort_service.dart'; -import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import "package:appflowy/generated/locale_keys.g.dart"; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy/plugins/database/domain/sort_service.dart'; +import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import '../../../../widgets/row/accessory/cell_accessory.dart'; import '../../../../widgets/row/cells/cell_container.dart'; import '../../layout/sizes.dart'; - import 'action.dart'; class GridRow extends StatelessWidget { @@ -35,6 +33,7 @@ class GridRow extends StatelessWidget { required this.openDetailPage, required this.index, this.shrinkWrap = false, + required this.editable, }); final FieldController fieldController; @@ -45,6 +44,7 @@ class GridRow extends StatelessWidget { final void Function(BuildContext context) openDetailPage; final int index; final bool shrinkWrap; + final bool editable; @override Widget build(BuildContext context) { @@ -58,7 +58,7 @@ class GridRow extends StatelessWidget { rowContent = Expanded(child: rowContent); } - return BlocProvider( + rowContent = BlocProvider( create: (_) => RowBloc( fieldController: fieldController, rowId: rowId, @@ -74,6 +74,14 @@ class GridRow extends StatelessWidget { ), ), ); + + if (!editable) { + rowContent = IgnorePointer( + child: rowContent, + ); + } + + return rowContent; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 09e6d7f2d5..c171c9a4c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -79,13 +79,19 @@ class DatabaseTabBarView extends StatelessWidget { ..add(const DatabaseTabBarEvent.initial()), ), BlocProvider( - create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), + create: (_) => ViewBloc(view: view) + ..add( + const ViewEvent.initial(), + ), ), ], child: BlocBuilder( - builder: (_, state) { + builder: (innerContext, state) { final layout = state.tabBars[state.selectedIndex].layout; - return Column( + final isLocked = + context.read()?.state.view.isLocked ?? false; + + final Widget child = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (UniversalPlatform.isMobile) const VSpace(12), @@ -99,9 +105,17 @@ class DatabaseTabBarView extends StatelessWidget { return const SizedBox.shrink(); } - return UniversalPlatform.isDesktop + Widget child = UniversalPlatform.isDesktop ? const TabBarHeader() : const MobileTabBarHeader(); + + if (innerContext.watch().state.view.isLocked) { + child = IgnorePointer( + child: child, + ); + } + + return child; }, ), pageSettingBarExtensionFromState(context, state), @@ -111,6 +125,12 @@ class DatabaseTabBarView extends StatelessWidget { ), ], ); + + if (isLocked) { + return IgnorePointer(child: child); + } + + return child; }, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 583af8af20..561d281154 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -16,6 +16,7 @@ import 'package:appflowy/workspace/application/action_navigation/action_navigati import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -63,6 +64,7 @@ class _DocumentPageState extends State void dispose() { WidgetsBinding.instance.removeObserver(this); documentBloc.close(); + super.dispose(); } @@ -82,31 +84,56 @@ class _DocumentPageState extends State providers: [ BlocProvider.value(value: getIt()), BlocProvider.value(value: documentBloc), + BlocProvider.value( + value: ViewLockStatusBloc(view: widget.view) + ..add( + ViewLockStatusEvent.initial(), + ), + ), ], - child: BlocBuilder( - buildWhen: shouldRebuildDocument, - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator.adaptive()); + child: BlocConsumer( + listenWhen: (prev, curr) => curr.isLocked != prev.isLocked, + listener: (context, lockStatusState) { + if (lockStatusState.isLoadingLockStatus) { + return; } + editorState?.editable = !lockStatusState.isLocked; + }, + builder: (context, lockStatusState) { + return BlocBuilder( + buildWhen: shouldRebuildDocument, + builder: (context, state) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } - final editorState = state.editorState; - this.editorState = editorState; - final error = state.error; - if (error != null || editorState == null) { - Log.error(error); - return Center(child: AppFlowyErrorPage(error: error)); - } + final editorState = state.editorState; + this.editorState = editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return Center(child: AppFlowyErrorPage(error: error)); + } - if (state.forceClose) { - widget.onDeleted(); - return const SizedBox.shrink(); - } + if (state.forceClose) { + widget.onDeleted(); + return const SizedBox.shrink(); + } - return BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: onNotificationAction, - child: buildEditorPage(context, state), + return BlocListener( + listener: (context, state) { + editorState.editable = !state.isLocked; + }, + child: + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: onNotificationAction, + child: buildEditorPage(context, state), + ), + ); + }, ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 207951fe22..a46be8530f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -14,6 +14,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -315,11 +316,13 @@ class _AppFlowyEditorPageState extends State ); final isViewDeleted = context.read().state.isDeleted; + final isLocked = + context.read()?.state.isLocked ?? false; final editor = Directionality( textDirection: textDirection, child: AppFlowyEditor( editorState: widget.editorState, - editable: !isViewDeleted, + editable: !isViewDeleted && !isLocked, editorScrollController: editorScrollController, // setup the auto focus parameters autoFocus: widget.autoFocus ?? autoFocus, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 0f114d7ff4..7f19e976d4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -71,6 +71,11 @@ extension ViewExtension on ViewPB { name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name; bool get isDocument => pluginType == PluginType.document; + bool get isDatabase => [ + PluginType.grid, + PluginType.board, + PluginType.calendar, + ].contains(pluginType); Widget defaultIcon({Size? size}) => FlowySvg( switch (layout) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart new file mode 100644 index 0000000000..251131d849 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; + +part 'view_lock_status_bloc.freezed.dart'; + +class ViewLockStatusBloc + extends Bloc { + ViewLockStatusBloc({ + required this.view, + }) : viewBackendSvc = ViewBackendService(), + listener = ViewListener(viewId: view.id), + super(ViewLockStatusState.init(view)) { + on( + (event, emit) async { + await event.when( + initial: () async { + listener.start( + onViewUpdated: (view) async { + add(ViewLockStatusEvent.updateLockStatus(view.isLocked)); + }, + ); + + final result = await ViewBackendService.getView(view.id); + final latestView = result.fold( + (view) => view, + (_) => view, + ); + emit( + state.copyWith( + view: latestView, + isLocked: latestView.isLocked, + isLoadingLockStatus: false, + ), + ); + }, + lock: () async { + final result = await ViewBackendService.lockView(view.id); + final isLocked = result.fold( + (_) => true, + (_) => false, + ); + add( + ViewLockStatusEvent.updateLockStatus( + isLocked, + ), + ); + }, + unlock: () async { + final result = await ViewBackendService.unlockView(view.id); + final isLocked = result.fold( + (_) => false, + (_) => true, + ); + add( + ViewLockStatusEvent.updateLockStatus( + isLocked, + lockCounter: state.lockCounter + 1, + ), + ); + }, + updateLockStatus: (isLocked, lockCounter) { + state.view.freeze(); + final updatedView = state.view.rebuild( + (update) => update.isLocked = isLocked, + ); + emit( + state.copyWith( + view: updatedView, + isLocked: isLocked, + lockCounter: lockCounter ?? state.lockCounter, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final ViewBackendService viewBackendSvc; + final ViewListener listener; + + @override + Future close() async { + await listener.stop(); + + return super.close(); + } +} + +@freezed +class ViewLockStatusEvent with _$ViewLockStatusEvent { + const factory ViewLockStatusEvent.initial() = Initial; + const factory ViewLockStatusEvent.lock() = Lock; + const factory ViewLockStatusEvent.unlock() = Unlock; + const factory ViewLockStatusEvent.updateLockStatus( + bool isLocked, { + int? lockCounter, + }) = UpdateLockStatus; +} + +@freezed +class ViewLockStatusState with _$ViewLockStatusState { + const factory ViewLockStatusState({ + required ViewPB view, + required bool isLocked, + required int lockCounter, + @Default(true) bool isLoadingLockStatus, + }) = _ViewLockStatusState; + + factory ViewLockStatusState.init(ViewPB view) => ViewLockStatusState( + view: view, + isLocked: false, + lockCounter: 0, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index d20aed8c1b..6599db1273 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -392,4 +392,14 @@ class ViewBackendService { return (publishedPages.isNotEmpty, publishedPages); } + + static Future> lockView(String viewId) async { + final payload = ViewIdPB()..value = viewId; + return FolderEventLockView(payload).send(); + } + + static Future> unlockView(String viewId) async { + final payload = ViewIdPB()..value = viewId; + return FolderEventUnlockView(payload).send(); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart index fe2e5def48..d4f91b67d9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -17,6 +17,14 @@ enum ViewMoreActionType { divider, lastModified, created, + lockPage; + + static const disableInLockedView = [ + delete, + rename, + moveTo, + changeIcon, + ]; } extension ViewMoreActionTypeExtension on ViewMoreActionType { @@ -42,6 +50,8 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return LocaleKeys.disclosureAction_changeIcon.tr(); case ViewMoreActionType.collapseAllPages: return LocaleKeys.disclosureAction_collapseAllPages.tr(); + case ViewMoreActionType.lockPage: + return LocaleKeys.disclosureAction_lockPage.tr(); case ViewMoreActionType.divider: case ViewMoreActionType.lastModified: case ViewMoreActionType.created: @@ -69,6 +79,8 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return FlowySvgs.change_icon_s; case ViewMoreActionType.collapseAllPages: return FlowySvgs.collapse_all_page_s; + case ViewMoreActionType.lockPage: + return FlowySvgs.lock_page_s; case ViewMoreActionType.divider: case ViewMoreActionType.lastModified: case ViewMoreActionType.copyLink: @@ -92,6 +104,7 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { case ViewMoreActionType.delete: case ViewMoreActionType.lastModified: case ViewMoreActionType.created: + case ViewMoreActionType.lockPage: return const SizedBox.shrink(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 34509dbb96..931d8d1a33 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -21,6 +21,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type. import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -634,7 +635,7 @@ class _SingleInnerViewItemState extends State { ? RawEmojiIconWidget(emoji: iconData, emojiSize: 16.0) : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); - return AppFlowyPopover( + final Widget child = AppFlowyPopover( offset: const Offset(20, 0), controller: controller, direction: PopoverDirection.rightWithCenterAligned, @@ -669,6 +670,14 @@ class _SingleInnerViewItemState extends State { ); }, ); + + if (widget.view.isLocked) { + return LockPageButtonWrapper( + child: child, + ); + } + + return child; } // > button or · button diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index a58aab2b2e..d792c54f04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -54,19 +55,22 @@ class ViewMoreActionPopover extends StatelessWidget { List _buildActionTypeWrappers() { final actionTypes = _buildActionTypes(); - return actionTypes - .map( - (e) => ViewMoreActionTypeWrapper(e, view, (controller, data) { - onEditing(false); - onAction(e, data); - bool enableClose = true; - if (data is SelectedEmojiIconResult) { - if (data.keepOpen) enableClose = false; - } - if (enableClose) controller.close(); - }), - ) - .toList(); + return actionTypes.map( + (e) { + final actionWrapper = + ViewMoreActionTypeWrapper(e, view, (controller, data) { + onEditing(false); + onAction(e, data); + bool enableClose = true; + if (data is SelectedEmojiIconResult) { + if (data.keepOpen) enableClose = false; + } + if (enableClose) controller.close(); + }); + + return actionWrapper; + }, + ).toList(); } List _buildActionTypes() { @@ -144,19 +148,30 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { PopoverController controller, PopoverMutex? mutex, ) { + Widget child; + if (inner == ViewMoreActionType.divider) { - return _buildDivider(); + child = _buildDivider(); } else if (inner == ViewMoreActionType.lastModified) { - return _buildLastModified(context); + child = _buildLastModified(context); } else if (inner == ViewMoreActionType.created) { - return _buildCreated(context); + child = _buildCreated(context); } else if (inner == ViewMoreActionType.changeIcon) { - return _buildEmojiActionButton(context, controller); + child = _buildEmojiActionButton(context, controller); } else if (inner == ViewMoreActionType.moveTo) { - return _buildMoveToActionButton(context, controller); + child = _buildMoveToActionButton(context, controller); + } else { + child = _buildNormalActionButton(context, controller); } - return _buildNormalActionButton(context, controller); + if (ViewMoreActionType.disableInLockedView.contains(inner) && + sourceView.isLocked) { + child = LockPageButtonWrapper( + child: child, + ); + } + + return child; } Widget _buildNormalActionButton( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 9b2f75ddd0..5b11e2d139 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -60,7 +60,7 @@ class _AccountDeletionButtonState extends State { const VSpace(8), Row( children: [ - Flexible( + Expanded( child: FlowyText.regular( LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(), fontSize: 12.0, @@ -69,7 +69,6 @@ class _AccountDeletionButtonState extends State { color: textColor, ), ), - const HSpace(32), FlowyTextButton( LocaleKeys.button_deleteAccount.tr(), constraints: const BoxConstraints(minHeight: 32), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 86ca1a3018..90e47e7c19 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -5,10 +5,12 @@ import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -62,18 +64,23 @@ class _MoreViewActionsState extends State { ); } - Widget _buildPopup(ViewInfoState state) { + Widget _buildPopup(ViewInfoState viewInfoState) { final userWorkspaceBloc = context.read(); final userProfile = userWorkspaceBloc.userProfile; final workspaceId = userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? ''; - final actions = _buildActions(state); return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + create: (_) => ViewBloc(view: widget.view) + ..add( + const ViewEvent.initial(), + ), + ), + BlocProvider( + create: (_) => ViewLockStatusBloc(view: widget.view) + ..add(ViewLockStatusEvent.initial()), ), BlocProvider( create: (context) => SpaceBloc( @@ -84,27 +91,36 @@ class _MoreViewActionsState extends State { ), ), ], - child: BlocBuilder( - builder: (context, state) { - if (state.spaces.isEmpty && - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { - return const SizedBox.shrink(); - } + child: BlocBuilder( + builder: (context, viewState) { + return BlocBuilder( + builder: (context, state) { + if (state.spaces.isEmpty && + userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { + return const SizedBox.shrink(); + } - return ListView.builder( - key: ValueKey(state.spaces.hashCode), - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: actions.length, - physics: StyledScrollPhysics(), - itemBuilder: (_, index) => actions[index], + final actions = _buildActions( + context, + viewInfoState, + ); + return ListView.builder( + key: ValueKey(state.spaces.hashCode), + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: actions.length, + physics: StyledScrollPhysics(), + itemBuilder: (_, index) => actions[index], + ); + }, ); }, ), ); } - List _buildActions(ViewInfoState state) { + List _buildActions(BuildContext context, ViewInfoState state) { + final view = context.watch().state.view; final appearanceSettings = context.watch().state; final dateFormat = appearanceSettings.dateFormat; final timeFormat = appearanceSettings.timeFormat; @@ -122,14 +138,24 @@ class _MoreViewActionsState extends State { const FontSizeAction(), ViewAction( type: ViewMoreActionType.divider, - view: widget.view, + view: view, + mutex: popoverMutex, + ), + ], + if (widget.view.isDocument || widget.view.isDatabase) ...[ + LockPageAction( + view: view, + ), + ViewAction( + type: ViewMoreActionType.divider, + view: view, mutex: popoverMutex, ), ], ...viewMoreActionTypes.map( (type) => ViewAction( type: type, - view: widget.view, + view: view, mutex: popoverMutex, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart new file mode 100644 index 0000000000..202919b639 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart @@ -0,0 +1,119 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LockPageAction extends StatefulWidget { + const LockPageAction({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State createState() => _LockPageActionState(); +} + +class _LockPageActionState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ViewLockStatusBloc(view: widget.view) + ..add( + ViewLockStatusEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return _buildTextButton(context); + }, + ), + ); + } + + Widget _buildTextButton( + BuildContext context, + ) { + return Container( + height: 34, + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: FlowyIconTextButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + onTap: () => _toggle(context), + leftIconBuilder: (onHover) => FlowySvg( + FlowySvgs.lock_page_s, + size: const Size.square(16.0), + ), + iconPadding: 10.0, + textBuilder: (onHover) => FlowyText( + LocaleKeys.disclosureAction_lockPage.tr(), + figmaLineHeight: 18.0, + ), + rightIconBuilder: (_) => _buildSwitch( + context, + ), + ), + ); + } + + Widget _buildSwitch(BuildContext context) { + final lockState = context.read().state; + if (lockState.isLoadingLockStatus) { + return SizedBox.shrink(); + } + + return Container( + width: 30, + height: 20, + margin: const EdgeInsets.only(right: 6), + child: FittedBox( + fit: BoxFit.fill, + child: CupertinoSwitch( + value: lockState.isLocked, + activeTrackColor: Theme.of(context).colorScheme.primary, + onChanged: (_) => _toggle(context), + ), + ), + ); + } + + Future _toggle(BuildContext context) async { + final isLocked = context.read().state.isLocked; + + context.read().add( + isLocked ? ViewLockStatusEvent.unlock() : ViewLockStatusEvent.lock(), + ); + + Log.info('update page(${widget.view.id}) lock status: $isLocked'); + } +} + +class LockPageButtonWrapper extends StatelessWidget { + const LockPageButtonWrapper({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.lockPage_lockedOperationTooltip.tr(), + child: IgnorePointer( + child: Opacity( + opacity: 0.5, + child: child, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 05657307f9..fa204d7dad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -5,10 +5,12 @@ import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart'; import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -29,8 +31,14 @@ class ViewTitleBar extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ViewTitleBarBloc(view: view), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => ViewTitleBarBloc(view: view)), + BlocProvider( + create: (_) => ViewLockStatusBloc(view: view) + ..add(const ViewLockStatusEvent.initial()), + ), + ], child: BlocBuilder( builder: (context, state) { final ancestors = state.ancestors; @@ -42,11 +50,14 @@ class ViewTitleBar extends StatelessWidget { child: SizedBox( height: 24, child: Row( - children: _buildViewTitles( - context, - ancestors, - state.isDeleted, - ), + children: [ + ..._buildViewTitles( + context, + ancestors, + state.isDeleted, + ), + _buildLockPageStatus(context), + ], ), ), ); @@ -55,6 +66,30 @@ class ViewTitleBar extends StatelessWidget { ); } + Widget _buildLockPageStatus(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.isLoadingLockStatus == current.isLoadingLockStatus && + current.isLoadingLockStatus == false, + listener: (context, state) { + if (state.isLocked) { + showToastNotification( + context, + message: LocaleKeys.lockPage_pageLockedToast.tr(), + ); + } + }, + builder: (context, state) { + if (state.isLocked) { + return _Lock(); + } else if (!state.isLocked && state.lockCounter > 0) { + return _ReLock(); + } + return const SizedBox.shrink(); + }, + ); + } + List _buildViewTitles( BuildContext context, List views, @@ -98,7 +133,7 @@ class ViewTitleBar extends StatelessWidget { message: view.name, child: ViewTitle( view: view, - behavior: i == views.length - 1 + behavior: i == views.length - 1 && !view.isLocked ? ViewTitleBehavior.editable // only the last one is editable : ViewTitleBehavior.uneditable, // others are not editable onUpdated: () { @@ -350,3 +385,78 @@ class _ViewTitleState extends State { ); } } + +class _Lock extends StatelessWidget { + const _Lock(); + + @override + Widget build(BuildContext context) { + final color = const Color(0xFFD95A0B); + return FlowyTooltip( + message: LocaleKeys.lockPage_lockTooltip.tr(), + child: DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: color), + borderRadius: BorderRadius.circular(6), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 4.0, + ), + iconPadding: 4.0, + text: FlowyText.regular( + LocaleKeys.lockPage_lockPage.tr(), + color: color, + fontSize: 12.0, + ), + hoverColor: color.withValues(alpha: 0.1), + leftIcon: FlowySvg(FlowySvgs.lock_page_s, color: color), + onTap: () => context.read().add( + const ViewLockStatusEvent.unlock(), + ), + ), + ), + ); + } +} + +class _ReLock extends StatelessWidget { + const _ReLock(); + + @override + Widget build(BuildContext context) { + final iconColor = const Color(0xFF8F959E); + return DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: iconColor), + borderRadius: BorderRadius.circular(6), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 4.0, + ), + iconPadding: 4.0, + text: FlowyText.regular( + LocaleKeys.lockPage_reLockPage.tr(), + fontSize: 12.0, + ), + leftIcon: FlowySvg( + FlowySvgs.unlock_page_s, + color: iconColor, + blendMode: null, + ), + onTap: () => context.read().add( + const ViewLockStatusEvent.lock(), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 3c5ca468af..c3dddcda0e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "55c457f" - resolved-ref: "55c457f472ed997906bd77142ef94bb7f66cc629" + ref: "9b5c461" + resolved-ref: "9b5c46153a769affc9e5d85ff91115796e97bce8" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 5643e3b09b..00aca414ea 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.2 +version: 0.8.4 environment: flutter: ">=3.27.4" @@ -178,7 +178,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "55c457f" + ref: "9b5c461" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/16x/lock_page.svg b/frontend/resources/flowy_icons/16x/lock_page.svg new file mode 100644 index 0000000000..b68b7ab42f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/lock_page.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/unlock_page.svg b/frontend/resources/flowy_icons/16x/unlock_page.svg new file mode 100644 index 0000000000..38f60dedb8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/unlock_page.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 98c0167327..32d8490524 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -174,7 +174,8 @@ "changeIcon": "Change icon", "collapseAllPages": "Collapse all subpages", "movePageTo": "Move page to", - "move": "Move" + "move": "Move", + "lockPage": "Lock page" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -3096,5 +3097,12 @@ "settingsUpdateDescription": "Current version: {currentVersion} (Official build) → {newVersion}", "settingsUpdateButton": "Update now", "settingsUpdateWhatsNew": "What's new" + }, + "lockPage": { + "lockPage": "Locked", + "reLockPage": "Re-lock", + "lockTooltip": "Page locked to prevent accidental editing. Click to unlock.", + "pageLockedToast": "Page locked. Editing is disabled until someone unlocks it.", + "lockedOperationTooltip": "Page locked to prevent accidental editing." } } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 9408978e26..d1a2733b1e 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "arc-swap", @@ -923,7 +923,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "async-trait", @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "arc-swap", @@ -984,7 +984,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "bytes", @@ -1004,7 +1004,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "arc-swap", @@ -1026,7 +1026,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "async-recursion", @@ -1090,7 +1090,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "async-stream", @@ -1170,7 +1170,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988" dependencies = [ "anyhow", "collab", @@ -1400,7 +1400,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -4624,7 +4624,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4644,7 +4644,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4712,19 +4711,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b2725230ac..b656d4f439 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 8ed2f3b4ea..acc145767e 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -365,6 +365,9 @@ pub enum ErrorCode { #[error("AI Max Required")] AIMaxRequired = 125, + + #[error("View is locked")] + ViewIsLocked = 126, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 34965e036e..507905dbad 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -149,6 +149,8 @@ impl FlowyError { static_flowy_error!(local_ai_unavailable, ErrorCode::LocalAIUnavailable); static_flowy_error!(response_timeout, ErrorCode::ResponseTimeout); static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded); + + static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 60df8158de..a8f331a91c 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -72,6 +72,11 @@ pub struct ViewPB { // user_id #[pb(index = 12, one_of)] pub last_edited_by: Option, + + // is_locked + // If true, the view is locked and cannot be edited. + #[pb(index = 13, one_of)] + pub is_locked: Option, } pub fn view_pb_without_child_views(view: View) -> ViewPB { @@ -88,6 +93,7 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB { created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, + is_locked: view.is_locked, } } @@ -105,6 +111,7 @@ pub fn view_pb_without_child_views_from_arc(view: Arc) -> ViewPB { created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, + is_locked: view.is_locked, } } @@ -126,6 +133,7 @@ pub fn view_pb_with_child_views(view: Arc, child_views: Vec>) -> created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, + is_locked: view.is_locked, } } diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 7e13274ca3..30cbd29d1c 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -533,3 +533,25 @@ pub(crate) async fn remove_default_publish_view_handler( folder.remove_default_published_view().await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip(data, folder))] +pub(crate) async fn lock_view_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let view_id = data.into_inner().value; + folder.lock_view(&view_id).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, folder))] +pub(crate) async fn unlock_view_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let view_id = data.into_inner().value; + folder.unlock_view(&view_id).await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 8078cdf896..abd74bd338 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -53,6 +53,8 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::GetDefaultPublishInfo, get_default_publish_info_handler) .event(FolderEvent::SetDefaultPublishView, set_default_publish_view_handler) .event(FolderEvent::RemoveDefaultPublishView, remove_default_publish_view_handler) + .event(FolderEvent::LockView, lock_view_handler) + .event(FolderEvent::UnlockView, unlock_view_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -220,4 +222,10 @@ pub enum FolderEvent { #[event()] RemoveDefaultPublishView = 53, + + #[event(input = "ViewIdPB")] + LockView = 54, + + #[event(input = "ViewIdPB")] + UnlockView = 55, } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 44509fbd2c..b9e41f2998 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -767,6 +767,11 @@ impl FolderManager { } if let Some(view) = folder.get_view(view_id) { + // if the view is locked, the view can't be moved to trash + if view.is_locked.unwrap_or(false) { + return Err(FlowyError::view_is_locked()); + } + Self::unfavorite_view_and_decendants(view.clone(), &mut folder); folder.add_trash_view_ids(vec![view_id.to_string()]); drop(folder); @@ -840,6 +845,11 @@ impl FolderManager { let from_section = params.from_section; let to_section = params.to_section; let view = self.get_view_pb(&view_id).await?; + // if the view is locked, the view can't be moved + if view.is_locked.unwrap_or(false) { + return Err(FlowyError::view_is_locked()); + } + let old_parent_id = view.parent_view_id; if let Some(lock) = self.mutex_folder.load_full() { let mut folder = lock.write().await; @@ -863,6 +873,12 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self), err)] pub async fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; + let view = self.get_view_pb(view_id).await?; + // if the view is locked, the view can't be moved + if view.is_locked.unwrap_or(false) { + return Err(FlowyError::view_is_locked()); + } + if let Some((is_workspace, parent_view_id, child_views)) = self.get_view_relation(view_id).await { // The display parent view is the view that is displayed in the UI @@ -952,7 +968,7 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self), err)] pub async fn update_view_with_params(&self, params: UpdateViewParams) -> FlowyResult<()> { self - .update_view(¶ms.view_id, |update| { + .update_view(¶ms.view_id, true, |update| { update .set_name_if_not_none(params.name) .set_desc_if_not_none(params.desc) @@ -971,12 +987,34 @@ impl FolderManager { params: UpdateViewIconParams, ) -> FlowyResult<()> { self - .update_view(¶ms.view_id, |update| { + .update_view(¶ms.view_id, true, |update| { update.set_icon(params.icon).done() }) .await } + /// Lock the view with the given view id. + /// + /// If the view is locked, it cannot be edited. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn lock_view(&self, view_id: &str) -> FlowyResult<()> { + self + .update_view(view_id, false, |update| { + update.set_page_lock_status(true).done() + }) + .await + } + + /// Unlock the view with the given view id. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn unlock_view(&self, view_id: &str) -> FlowyResult<()> { + self + .update_view(view_id, false, |update| { + update.set_page_lock_status(false).done() + }) + .await + } + /// Duplicate the view with the given view id. /// /// Including the view data (icon, cover, extra) and the child views. @@ -1791,7 +1829,10 @@ impl FolderManager { } /// Update the view with the provided view_id using the specified function. - async fn update_view(&self, view_id: &str, f: F) -> FlowyResult<()> + /// + /// If the check_locked is true, it will check the lock status of the view. If the view is locked, + /// it will return an error. + async fn update_view(&self, view_id: &str, check_locked: bool, f: F) -> FlowyResult<()> where F: FnOnce(ViewUpdate) -> Option, { @@ -1801,6 +1842,12 @@ impl FolderManager { Some(lock) => { let mut folder = lock.write().await; let old_view = folder.get_view(view_id); + + // Check if the view is locked + if check_locked && old_view.as_ref().and_then(|v| v.is_locked).unwrap_or(false) { + return Err(FlowyError::view_is_locked()); + } + let new_view = folder.update_view(view_id, f); Some((old_view, new_view)) diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index a4f2792afc..2b0a9667c9 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -164,6 +164,7 @@ pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout last_edited_by: Some(uid), extra: params.extra, children: Default::default(), + is_locked: None, } } From c1a8d899384c1b951c5f8783e015ec5895b1180b Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 12 Feb 2025 15:07:21 +0800 Subject: [PATCH 026/384] feat: lock page on mobile (#7366) * feat: support lock button in view more actions * feat: add lock page on mobile * feat: disable actions in locked page * feat: disable more actions in locked page * feat: support locked grid on mobile * feat: support locked board/calendar on mobile * fix: exclude lock page button from AI Chat --- frontend/appflowy_flutter/ios/Podfile.lock | 23 +-- .../ios/Runner/AppDelegate.swift | 2 +- .../presentation/base/mobile_view_page.dart | 132 +++++++++++++++--- .../base/view_page/app_bar_buttons.dart | 19 ++- .../base/view_page/more_bottom_sheet.dart | 25 +++- .../bottom_sheet_view_item_body.dart | 6 + .../bottom_sheet/bottom_sheet_view_page.dart | 84 ++++++++++- .../default_mobile_action_pane.dart | 6 + .../database/board/mobile_board_page.dart | 120 +++++++++------- .../flowy_mobile_quick_action_button.dart | 52 ++++--- .../widgets/flowy_option_tile.dart | 24 +++- .../calendar/presentation/calendar_page.dart | 3 +- .../grid/presentation/mobile_grid_page.dart | 17 ++- .../widgets/header/mobile_grid_header.dart | 18 ++- .../database/tab_bar/tab_bar_view.dart | 6 - .../document/presentation/editor_page.dart | 3 + .../presentation/widgets/view_title_bar.dart | 12 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 19 files changed, 419 insertions(+), 139 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index e4e87805cd..c87e0512f8 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - app_links (0.0.1): + - app_links (0.0.2): - Flutter - appflowy_backend (0.0.1): - Flutter @@ -79,7 +79,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - super_native_extensions (0.0.1): @@ -90,6 +90,7 @@ PODS: - Flutter - webview_flutter_wkwebview (0.0.1): - Flutter + - FlutterMacOS DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) @@ -111,10 +112,10 @@ DEPENDENCIES: - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -165,17 +166,17 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" super_native_extensions: :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 + app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 @@ -186,7 +187,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 @@ -199,12 +200,12 @@ SPEC CHECKSUMS: sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift b/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift index 70693e4a8c..b636303481 100644 --- a/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 0535ec76b8..a02f3879cd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; @@ -7,6 +8,7 @@ import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; @@ -18,6 +20,9 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -91,7 +96,7 @@ class _MobileViewPageState extends State { final body = _buildBody(context, state); if (view == null) { - return _buildApp(context, null, body); + return SizedBox.shrink(); } return MultiBlocProvider( @@ -122,6 +127,11 @@ class _MobileViewPageState extends State { create: (_) => DocumentPageStyleBloc(view: view) ..add(const DocumentPageStyleEvent.initial()), ), + if (view.layout.isDocumentView || view.layout.isDatabaseView) + BlocProvider( + create: (_) => ViewLockStatusBloc(view: view) + ..add(const ViewLockStatusEvent.initial()), + ), ], child: Builder( builder: (context) { @@ -152,6 +162,7 @@ class _MobileViewPageState extends State { title: title, appBarOpacity: _appBarOpacity, actions: actions, + view: view, ) : FlowyAppBar(title: title, actions: actions); final body = isDocument @@ -222,6 +233,8 @@ class _MobileViewPageState extends State { final isImmersiveMode = context.read().state.isImmersiveMode; + final isLocked = + context.read()?.state.isLocked ?? false; final actions = []; if (FeatureFlag.syncDocument.isOn) { @@ -240,7 +253,7 @@ class _MobileViewPageState extends State { } } - if (view.layout.isDocumentView) { + if (view.layout.isDocumentView && !isLocked) { actions.addAll([ MobileViewPageLayoutButton( view: view, @@ -270,25 +283,104 @@ class _MobileViewPageState extends State { Widget _buildTitle(BuildContext context, ViewPB? view) { final icon = view?.icon; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null && icon.value.isNotEmpty) ...[ - RawEmojiIconWidget( - emoji: icon.toEmojiIconData(), - emojiSize: 15, + return ValueListenableBuilder( + valueListenable: _appBarOpacity, + builder: (_, value, child) { + if (value < 0.99) { + return Padding( + padding: const EdgeInsets.only(left: 6.0), + child: _buildLockStatus(context, view), + ); + } + + return Opacity( + opacity: value, + child: Row( + children: [ + if (icon != null && icon.value.isNotEmpty) ...[ + RawEmojiIconWidget( + emoji: icon.toEmojiIconData(), + emojiSize: 15, + ), + const HSpace(4), + ], + FlowyText.medium( + widget.fixedTitle ?? view?.name ?? widget.title ?? '', + fontSize: 15.0, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 18.0, + ), + const HSpace(4.0), + _buildLockStatusIcon(context, view), + ], ), - const HSpace(4), - ], - Expanded( - child: FlowyText.medium( - widget.fixedTitle ?? view?.name ?? widget.title ?? '', - fontSize: 15.0, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 18.0, - ), - ), - ], + ); + }, + ); + } + + Widget _buildLockStatus(BuildContext context, ViewPB? view) { + if (view == null || view.layout == ViewLayoutPB.Chat) { + return const SizedBox.shrink(); + } + + return BlocConsumer( + listenWhen: (previous, current) => + previous.isLoadingLockStatus == current.isLoadingLockStatus && + current.isLoadingLockStatus == false, + listener: (context, state) { + if (state.isLocked) { + showToastNotification( + context, + message: LocaleKeys.lockPage_pageLockedToast.tr(), + ); + + EditorNotification.exitEditing().post(); + } + }, + builder: (context, state) { + if (state.isLocked) { + return LockedPageStatus(); + } else if (!state.isLocked && state.lockCounter > 0) { + return ReLockedPageStatus(); + } + return const SizedBox.shrink(); + }, + ); + } + + Widget _buildLockStatusIcon(BuildContext context, ViewPB? view) { + if (view == null || view.layout == ViewLayoutPB.Chat) { + return const SizedBox.shrink(); + } + + return BlocConsumer( + listenWhen: (previous, current) => + previous.isLoadingLockStatus == current.isLoadingLockStatus && + current.isLoadingLockStatus == false, + listener: (context, state) { + if (state.isLocked) { + showToastNotification( + context, + message: LocaleKeys.lockPage_pageLockedToast.tr(), + ); + } + }, + builder: (context, state) { + if (state.isLocked) { + return FlowySvg( + FlowySvgs.lock_page_s, + color: const Color(0xFFD95A0B), + ); + } else if (!state.isLocked && state.lockCounter > 0) { + return FlowySvg( + FlowySvgs.unlock_page_s, + color: Color(0xFF8F959E), + blendMode: null, + ); + } + return const SizedBox.shrink(); + }, ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index 2d52426bf9..a91fbf577b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -12,6 +12,7 @@ import 'package:appflowy/plugins/shared/share/share_bloc.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -28,12 +29,13 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget required this.appBarOpacity, required this.title, required this.actions, + required this.view, }); final ValueListenable appBarOpacity; final Widget title; final List actions; - + final ViewPB? view; @override final Size preferredSize; @@ -45,7 +47,7 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget backgroundColor: AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity), showDivider: false, - title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title), + title: _buildTitle(context, opacity: opacity), leadingWidth: 44, leading: Padding( padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0), @@ -56,6 +58,13 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget ); } + Widget _buildTitle( + BuildContext context, { + required double opacity, + }) { + return title; + } + Widget _buildAppBarBackButton(BuildContext context) { return AppBarButton( padding: EdgeInsets.zero, @@ -102,6 +111,12 @@ class MobileViewPageMoreButton extends StatelessWidget { BlocProvider.value(value: context.read()), BlocProvider.value(value: context.read()), BlocProvider.value(value: context.read()), + BlocProvider( + create: (context) => ViewLockStatusBloc(view: view) + ..add( + ViewLockStatusEvent.initial(), + ), + ), ], child: MobileViewPageMoreBottomSheet(view: view), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index 25541ed7d4..dd659420d6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -14,6 +14,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -43,7 +44,8 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { }, child: ViewPageBottomSheet( view: view, - onAction: (action) async => _onAction(context, action), + onAction: (action, {arguments}) async => + _onAction(context, action, arguments), onRename: (name) { _onRename(context, name); context.pop(); @@ -56,6 +58,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { Future _onAction( BuildContext context, MobileViewBottomSheetBodyAction action, + Map? arguments, ) async { switch (action) { case MobileViewBottomSheetBodyAction.duplicate: @@ -107,12 +110,32 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { break; case MobileViewBottomSheetBodyAction.updatePathName: _updatePathName(context); + case MobileViewBottomSheetBodyAction.lockPage: + final isLocked = + arguments?[MobileViewBottomSheetBodyActionArguments.isLockedKey] ?? + false; + await _lockPage(context, isLocked: isLocked); + // context.pop(); + break; case MobileViewBottomSheetBodyAction.rename: // no need to implement, rename is handled by the onRename callback. throw UnimplementedError(); } } + Future _lockPage( + BuildContext context, { + required bool isLocked, + }) async { + if (isLocked) { + context.read().add(const ViewLockStatusEvent.lock()); + } else { + context + .read() + .add(const ViewLockStatusEvent.unlock()); + } + } + Future _publish(BuildContext context) async { final id = context.read().view.id; final lastPublishName = context.read().state.pathName; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index a078521aec..0ca60fe40b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -1,8 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; enum MobileViewItemBottomSheetBodyAction { rename, @@ -40,6 +42,8 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { BuildContext context, MobileViewItemBottomSheetBodyAction action, ) { + final isLocked = + context.read()?.state.isLocked ?? false; switch (action) { case MobileViewItemBottomSheetBodyAction.rename: return FlowyOptionTile.text( @@ -49,6 +53,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { FlowySvgs.view_item_rename_s, size: Size.square(18), ), + enable: !isLocked, showTopBorder: false, showBottomBorder: false, onTap: () => onAction( @@ -94,6 +99,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { size: const Size.square(18), color: Theme.of(context).colorScheme.error, ), + enable: !isLocked, showTopBorder: false, showBottomBorder: false, onTap: () => onAction( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 2e08d0f368..e2aa34a648 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -4,9 +4,12 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/shared/share/share_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,11 +28,27 @@ enum MobileViewBottomSheetBodyAction { visitSite, copyShareLink, updatePathName, + lockPage; + + static const disableInLockedView = [ + undo, + redo, + rename, + delete, + ]; +} + +class MobileViewBottomSheetBodyActionArguments { + static const isLockedKey = 'is_locked'; } typedef MobileViewBottomSheetBodyActionCallback = void Function( MobileViewBottomSheetBodyAction action, -); + // for the [MobileViewBottomSheetBodyAction.lockPage] action, + // it will pass the [isLocked] value to the callback. + { + Map? arguments, +}); class ViewPageBottomSheet extends StatefulWidget { const ViewPageBottomSheet({ @@ -56,7 +75,7 @@ class _ViewPageBottomSheetState extends State { case MobileBottomSheetType.view: return MobileViewBottomSheetBody( view: widget.view, - onAction: (action) { + onAction: (action, {arguments}) { switch (action) { case MobileViewBottomSheetBodyAction.rename: setState(() { @@ -64,7 +83,7 @@ class _ViewPageBottomSheetState extends State { }); break; default: - widget.onAction(action); + widget.onAction(action, arguments: arguments); } }, ); @@ -93,6 +112,8 @@ class MobileViewBottomSheetBody extends StatelessWidget { @override Widget build(BuildContext context) { final isFavorite = view.isFavorite; + final isLocked = + context.watch()?.state.isLocked ?? false; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -100,6 +121,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { text: LocaleKeys.button_rename.tr(), icon: FlowySvgs.view_item_rename_s, iconSize: const Size.square(18), + enable: !isLocked, onTap: () => onAction( MobileViewBottomSheetBodyAction.rename, ), @@ -118,6 +140,28 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ), _divider(), + if (view.layout.isDatabaseView || view.layout.isDocumentView) ...[ + MobileQuickActionButton( + text: LocaleKeys.disclosureAction_lockPage.tr(), + icon: FlowySvgs.lock_page_s, + iconSize: const Size.square(18), + rightIconBuilder: (context) => _LockPageRightIconBuilder( + onAction: onAction, + ), + onTap: () { + final isLocked = + context.read()?.state.isLocked ?? false; + onAction( + MobileViewBottomSheetBodyAction.lockPage, + arguments: { + MobileViewBottomSheetBodyActionArguments.isLockedKey: + !isLocked, + }, + ); + }, + ), + _divider(), + ], MobileQuickActionButton( text: LocaleKeys.button_duplicate.tr(), icon: FlowySvgs.duplicate_s, @@ -144,6 +188,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { icon: FlowySvgs.trash_s, iconColor: Theme.of(context).colorScheme.error, iconSize: const Size.square(18), + enable: !isLocked, onTap: () => onAction( MobileViewBottomSheetBodyAction.delete, ), @@ -206,3 +251,36 @@ class MobileViewBottomSheetBody extends StatelessWidget { Widget _divider() => const MobileQuickActionDivider(); } + +class _LockPageRightIconBuilder extends StatelessWidget { + const _LockPageRightIconBuilder({ + required this.onAction, + }); + + final MobileViewBottomSheetBodyActionCallback onAction; + + @override + Widget build(BuildContext context) { + final isLocked = + context.watch()?.state.isLocked ?? false; + return SizedBox( + width: 46, + height: 30, + child: FittedBox( + fit: BoxFit.fill, + child: CupertinoSwitch( + value: isLocked, + activeTrackColor: Theme.of(context).colorScheme.primary, + onChanged: (value) { + onAction( + MobileViewBottomSheetBodyAction.lockPage, + arguments: { + MobileViewBottomSheetBodyActionArguments.isLockedKey: !value, + }, + ); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index faf02707b7..cb840b0f40 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -131,6 +132,11 @@ enum MobilePaneActionType { BlocProvider.value(value: favoriteBloc), if (recentViewsBloc != null) BlocProvider.value(value: recentViewsBloc), + BlocProvider( + create: (_) => + ViewLockStatusBloc(view: viewBloc.state.view) + ..add(const ViewLockStatusEvent.initial()), + ), ], child: BlocBuilder( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart index 2cfd807174..29841dd22a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart @@ -11,6 +11,7 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobi import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -142,6 +143,8 @@ class _BoardContentState extends State<_BoardContent> { return state.maybeMap( orElse: () => const SizedBox.shrink(), ready: (state) { + final isLocked = + context.watch()?.state.isLocked ?? false; final showCreateGroupButton = context .read() .groupingFieldType @@ -159,15 +162,20 @@ class _BoardContentState extends State<_BoardContent> { padding: config.groupHeaderPadding, ) : const HSpace(16), - trailing: showCreateGroupButton + trailing: showCreateGroupButton && !isLocked ? const MobileBoardTrailing() : const HSpace(16), - headerBuilder: (_, groupData) => BlocProvider.value( - value: context.read(), - child: GroupCardHeader( - groupData: groupData, - ), - ), + headerBuilder: (_, groupData) { + final isLocked = + context.read()?.state.isLocked ?? + false; + return IgnorePointer( + ignoring: isLocked, + child: GroupCardHeader( + groupData: groupData, + ), + ); + }, footerBuilder: _buildFooter, cardBuilder: (_, column, columnItem) => _buildCard( context: context, @@ -183,34 +191,39 @@ class _BoardContentState extends State<_BoardContent> { } Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { + final isLocked = + context.read()?.state.isLocked ?? false; final style = Theme.of(context); return SizedBox( height: 42, width: double.infinity, - child: TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.only(left: 8), - alignment: Alignment.centerLeft, - ), - icon: FlowySvg( - FlowySvgs.add_m, - color: style.colorScheme.onSurface, - ), - label: Text( - LocaleKeys.board_column_createNewCard.tr(), - style: style.textTheme.bodyMedium?.copyWith( + child: IgnorePointer( + ignoring: isLocked, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 8), + alignment: Alignment.centerLeft, + ), + icon: FlowySvg( + FlowySvgs.add_m, color: style.colorScheme.onSurface, ), - ), - onPressed: () => context.read().add( - BoardEvent.createRow( - columnData.id, - OrderObjectPositionTypePB.End, - null, - null, - ), + label: Text( + LocaleKeys.board_column_createNewCard.tr(), + style: style.textTheme.bodyMedium?.copyWith( + color: style.colorScheme.onSurface, ), + ), + onPressed: () => context.read().add( + BoardEvent.createRow( + columnData.id, + OrderObjectPositionTypePB.End, + null, + null, + ), + ), + ), ), ); } @@ -230,6 +243,8 @@ class _BoardContentState extends State<_BoardContent> { CardCellBuilder(databaseController: boardBloc.databaseController); final groupItemId = groupItem.row.id + groupData.group.groupId; + final isLocked = + context.read()?.state.isLocked ?? false; return Container( key: ValueKey(groupItemId), @@ -237,31 +252,34 @@ class _BoardContentState extends State<_BoardContent> { decoration: _makeBoxDecoration(context), child: BlocProvider.value( value: boardBloc, - child: RowCard( - fieldController: boardBloc.fieldController, - rowMeta: rowMeta, - viewId: boardBloc.viewId, - rowCache: boardBloc.rowCache, - groupingFieldId: groupItem.fieldInfo.id, - isEditing: false, - cellBuilder: cellBuilder, - onTap: (context) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: rowMeta.id, - MobileRowDetailPage.argDatabaseController: - context.read().databaseController, - }, - ); - }, - onStartEditing: () {}, - onEndEditing: () {}, - styleConfiguration: RowCardStyleConfiguration( - cellStyleMap: mobileBoardCardCellStyleMap(context), - showAccessory: false, + child: IgnorePointer( + ignoring: isLocked, + child: RowCard( + fieldController: boardBloc.fieldController, + rowMeta: rowMeta, + viewId: boardBloc.viewId, + rowCache: boardBloc.rowCache, + groupingFieldId: groupItem.fieldInfo.id, + isEditing: false, + cellBuilder: cellBuilder, + onTap: (context) { + context.push( + MobileRowDetailPage.routeName, + extra: { + MobileRowDetailPage.argRowId: rowMeta.id, + MobileRowDetailPage.argDatabaseController: + context.read().databaseController, + }, + ); + }, + onStartEditing: () {}, + onEndEditing: () {}, + styleConfiguration: RowCardStyleConfiguration( + cellStyleMap: mobileBoardCardCellStyleMap(context), + showAccessory: false, + ), + userProfile: boardBloc.userProfile, ), - userProfile: boardBloc.userProfile, ), ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index b447ed3d57..191deb1e9f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -12,6 +12,7 @@ class MobileQuickActionButton extends StatelessWidget { this.iconColor, this.iconSize, this.enable = true, + this.rightIconBuilder, }); final VoidCallback onTap; @@ -21,34 +22,39 @@ class MobileQuickActionButton extends StatelessWidget { final Color? iconColor; final Size? iconSize; final bool enable; + final WidgetBuilder? rightIconBuilder; @override Widget build(BuildContext context) { final iconSize = this.iconSize ?? const Size.square(18); - return InkWell( - onTap: enable ? onTap : null, - overlayColor: - enable ? null : const WidgetStatePropertyAll(Colors.transparent), - splashColor: Colors.transparent, - child: Container( - height: 52, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - FlowySvg( - icon, - size: iconSize, - color: enable ? iconColor : Theme.of(context).disabledColor, - ), - HSpace(30 - iconSize.width), - Expanded( - child: FlowyText.regular( - text, - fontSize: 16, - color: enable ? textColor : Theme.of(context).disabledColor, + return Opacity( + opacity: enable ? 1.0 : 0.5, + child: InkWell( + onTap: enable ? onTap : null, + overlayColor: + enable ? null : const WidgetStatePropertyAll(Colors.transparent), + splashColor: Colors.transparent, + child: Container( + height: 52, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + FlowySvg( + icon, + size: iconSize, + color: iconColor, ), - ), - ], + HSpace(30 - iconSize.width), + Expanded( + child: FlowyText.regular( + text, + fontSize: 16, + color: textColor, + ), + ), + if (rightIconBuilder != null) rightIconBuilder!(context), + ], + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index 835b3b052f..f38c724a22 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -37,6 +37,7 @@ class FlowyOptionTile extends StatelessWidget { this.backgroundColor, this.fontFamily, this.height, + this.enable = true, }); factory FlowyOptionTile.text({ @@ -49,6 +50,7 @@ class FlowyOptionTile extends StatelessWidget { Widget? trailing, VoidCallback? onTap, double? height, + bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.text, @@ -61,6 +63,7 @@ class FlowyOptionTile extends StatelessWidget { leading: leftIcon, trailing: trailing, height: height, + enable: enable, ); } @@ -77,6 +80,7 @@ class FlowyOptionTile extends StatelessWidget { Widget? trailing, String? textFieldHintText, bool autofocus = false, + bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.textField, @@ -90,6 +94,7 @@ class FlowyOptionTile extends StatelessWidget { onTextChanged: onTextChanged, onTextSubmitted: onTextSubmitted, autofocus: autofocus, + enable: enable, ); } @@ -105,6 +110,7 @@ class FlowyOptionTile extends StatelessWidget { bool showBottomBorder = true, String? fontFamily, Color? backgroundColor, + bool enable = true, }) { return FlowyOptionTile._( key: key, @@ -119,6 +125,7 @@ class FlowyOptionTile extends StatelessWidget { showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, leading: leftIcon, + enable: enable, trailing: isSelected ? const FlowySvg( FlowySvgs.m_blue_check_s, @@ -136,6 +143,7 @@ class FlowyOptionTile extends StatelessWidget { bool showTopBorder = true, bool showBottomBorder = true, Widget? leftIcon, + bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.toggle, @@ -146,6 +154,7 @@ class FlowyOptionTile extends StatelessWidget { showBottomBorder: showBottomBorder, leading: leftIcon, trailing: _Toggle(value: isSelected, onChanged: onValueChanged), + enable: enable, ); } @@ -181,11 +190,13 @@ class FlowyOptionTile extends StatelessWidget { final double? height; + final bool enable; + @override Widget build(BuildContext context) { final leadingWidget = _buildLeading(); - final child = FlowyOptionDecorateBox( + Widget child = FlowyOptionDecorateBox( color: backgroundColor, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, @@ -209,12 +220,21 @@ class FlowyOptionTile extends StatelessWidget { if (type == FlowyOptionTileType.checkbox || type == FlowyOptionTileType.toggle || type == FlowyOptionTileType.text) { - return GestureDetector( + child = GestureDetector( onTap: onTap, child: child, ); } + if (!enable) { + child = Opacity( + opacity: 0.5, + child: IgnorePointer( + child: child, + ), + ); + } + return child; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 7ecc306e7a..1876332d01 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -379,7 +379,8 @@ class _CalendarPageState extends State { // is implemnted in the develop branch(WIP). Will be replaced with that. final events = calenderEvents.map((value) => value.event!).toList() ..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp)); - final isLocked = context.watch().state.isLocked; + final isLocked = + context.watch()?.state.isLocked ?? false; return IgnorePointer( ignoring: isLocked, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart index 3874dc801e..17e4c0ed1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -182,6 +183,8 @@ class _GridPageContentState extends State { @override Widget build(BuildContext context) { + final isLocked = + context.read()?.state.isLocked ?? false; return BlocListener( listenWhen: (previous, current) => previous.createdRow != current.createdRow, @@ -215,7 +218,7 @@ class _GridPageContentState extends State { ), ], ), - if (!widget.shrinkWrap) + if (!widget.shrinkWrap && !isLocked) Positioned( bottom: 16, right: 16, @@ -356,7 +359,7 @@ class _GridRows extends StatelessWidget { final databaseController = context.read().databaseController; - final child = MobileGridRow( + Widget child = MobileGridRow( key: ValueKey(rowMeta.id), rowId: rowId, isDraggable: isDraggable, @@ -373,12 +376,20 @@ class _GridRows extends StatelessWidget { ); if (animation != null) { - return SizeTransition( + child = SizeTransition( sizeFactor: animation, child: child, ); } + final isLocked = + context.read()?.state.isLocked ?? false; + if (isLocked) { + child = IgnorePointer( + child: child, + ); + } + return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart index e20beba726..369bdeb523 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -39,6 +40,8 @@ class _MobileGridHeaderState extends State { Widget build(BuildContext context) { final fieldController = context.read().databaseController.fieldController; + final isLocked = + context.read()?.state.isLocked ?? false; return BlocProvider( create: (context) { return GridHeaderBloc( @@ -76,12 +79,15 @@ class _MobileGridHeaderState extends State { ); }, ), - SizedBox( - height: _kGridHeaderHeight, - child: _GridHeader( - viewId: widget.viewId, - fieldController: fieldController, - scrollController: widget.reorderableController, + IgnorePointer( + ignoring: isLocked, + child: SizedBox( + height: _kGridHeaderHeight, + child: _GridHeader( + viewId: widget.viewId, + fieldController: fieldController, + scrollController: widget.reorderableController, + ), ), ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index c171c9a4c1..7e009cb7f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -88,8 +88,6 @@ class DatabaseTabBarView extends StatelessWidget { child: BlocBuilder( builder: (innerContext, state) { final layout = state.tabBars[state.selectedIndex].layout; - final isLocked = - context.read()?.state.view.isLocked ?? false; final Widget child = Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -126,10 +124,6 @@ class DatabaseTabBarView extends StatelessWidget { ], ); - if (isLocked) { - return IgnorePointer(child: child); - } - return child; }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index a46be8530f..1425c2867c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -323,6 +323,8 @@ class _AppFlowyEditorPageState extends State child: AppFlowyEditor( editorState: widget.editorState, editable: !isViewDeleted && !isLocked, + disableSelectionService: UniversalPlatform.isMobile && isLocked, + disableKeyboardService: UniversalPlatform.isMobile && isLocked, editorScrollController: editorScrollController, // setup the auto focus parameters autoFocus: widget.autoFocus ?? autoFocus, @@ -348,6 +350,7 @@ class _AppFlowyEditorPageState extends State contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, + footer: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index fa204d7dad..1a1c993248 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -81,9 +81,9 @@ class ViewTitleBar extends StatelessWidget { }, builder: (context, state) { if (state.isLocked) { - return _Lock(); + return LockedPageStatus(); } else if (!state.isLocked && state.lockCounter > 0) { - return _ReLock(); + return ReLockedPageStatus(); } return const SizedBox.shrink(); }, @@ -386,8 +386,8 @@ class _ViewTitleState extends State { } } -class _Lock extends StatelessWidget { - const _Lock(); +class LockedPageStatus extends StatelessWidget { + const LockedPageStatus({super.key}); @override Widget build(BuildContext context) { @@ -424,8 +424,8 @@ class _Lock extends StatelessWidget { } } -class _ReLock extends StatelessWidget { - const _ReLock(); +class ReLockedPageStatus extends StatelessWidget { + const ReLockedPageStatus({super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c3dddcda0e..fd9c028f5a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9b5c461" - resolved-ref: "9b5c46153a769affc9e5d85ff91115796e97bce8" + ref: "6f5c957" + resolved-ref: "6f5c957049c90d942e56314171c7630b3d6d858d" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 00aca414ea..244a8dfa9c 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -178,7 +178,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "9b5c461" + ref: "6f5c957" appflowy_editor_plugins: git: From 189faa4def00263b34a2985f5c80932c6d41517c Mon Sep 17 00:00:00 2001 From: Reagan lee Date: Wed, 12 Feb 2025 15:08:01 +0800 Subject: [PATCH 027/384] chore: update translations (#7351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 4 +- frontend/resources/translations/ckb-KU.json | 4 +- frontend/resources/translations/fr-FR.json | 4 +- frontend/resources/translations/zh-CN.json | 58 ++++++++++++++++++--- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 133f6f6c69..4b4e112bce 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -2193,11 +2193,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل" + "clickToOpen": "انقر لفتح السجل", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index af7536b951..4acb7a1765 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -54,8 +54,8 @@ "LogInWithGoogle": "چوونە ژوورەوە لە ڕێگەی گووگڵەوە", "LogInWithGithub": "چوونە ژوورەوە لە ڕێگەی گیتهاب", "LogInWithDiscord": "چوونە ژوورەوە لە ڕێگەی دیسکۆرد", - "logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە", - "loginAsGuestButtonText": "دەست پێ بکە" + "loginAsGuestButtonText": "دەست پێ بکە", + "logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە" }, "workspace": { "chooseWorkspace": "هەڵبژاردنی شوێنی کارەکەت", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 068c3dbade..326d2d044f 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -72,8 +72,8 @@ "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", - "logInWithMagicLink": "Connectez-vous avec Magic Link", - "loginAsGuestButtonText": "Commencer" + "loginAsGuestButtonText": "Commencer", + "logInWithMagicLink": "Connectez-vous avec Magic Link" }, "workspace": { "chooseWorkspace": "Choisissez votre espace de travail", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index b3daad9fa2..f03f248a1c 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -72,13 +72,14 @@ "LogInWithGoogle": "使用 Google 登录", "LogInWithGithub": "使用 Github 登录", "LogInWithDiscord": "使用 Discord 登录", - "logInWithMagicLink": "使用魔法链接登录", - "loginAsGuestButtonText": "开始使用" + "loginAsGuestButtonText": "开始使用", + "logInWithMagicLink": "使用魔法链接登录" }, "workspace": { "chooseWorkspace": "选择您的工作区", "defaultName": "我的工作区", "create": "新建工作区", + "new": "新的工作空间", "importFromNotion": "从 Notion 导入", "learnMore": "了解更多", "reset": "重置工作区", @@ -149,7 +150,9 @@ "charCount": "字符数:{}", "createdAt": "创建于 :{}", "deleteView": "删除", - "duplicateView": "复制" + "duplicateView": "复制", + "wordCountLabel": "词总数:", + "charCountLabel": "字符总数:" }, "importPanel": { "textAndMarkdown": "文本 和 Markdown", @@ -170,7 +173,9 @@ "addToFavorites": "添加到收藏夹", "copyLink": "复制链接", "changeIcon": "更改图标", - "collapseAllPages": "收起全部子页面" + "collapseAllPages": "收起全部子页面", + "movePageTo": "将页面移动至", + "move": "移动" }, "blankPageTitle": "空白页", "newPageText": "新页面", @@ -186,6 +191,7 @@ "relatedQuestion": "相关问题", "serverUnavailable": "服务暂时不可用,请稍后再试", "aiServerUnavailable": "🌈 不妙!🌈 一只独角兽吃掉了我们的回复。请重试!", + "retry": "重试", "clickToRetry": "点击重试", "regenerateAnswer": "重新生成", "question1": "如何使用 Kanban 来管理任务", @@ -200,6 +206,11 @@ "uploadFile": "上传聊天使用的 PDF、md 或 txt 文件", "questionDetail": "{} 你好!我能怎么帮到你?", "indexingFile": "正在索引 {}", + "regenerate": "请重试", + "openPagePreviewFailedToast": "打开页面失败", + "changeFormat": { + "actionButton": "变更样式" + }, "referenceSource": "找到 {} 个来源", "referenceSources": "找到 {} 个来源", "questionTitle": "想法" @@ -258,7 +269,8 @@ "moreButtonToolTip": "删除、重命名等等...", "addPageTooltip": "在其中快速添加页面", "defaultNewPageName": "未命名页面", - "renameDialog": "重命名" + "renameDialog": "重命名", + "pageNameSuffix": "复制" }, "noPagesInside": "里面没有页面", "toolbar": { @@ -375,6 +387,7 @@ "upload": "上传", "edit": "编辑", "delete": "删除", + "copy": "复制", "duplicate": "复制", "putback": "放回去", "update": "更新", @@ -413,6 +426,8 @@ "viewing": "查看", "editing": "编辑", "gotIt": "我知道了", + "retry": "重试", + "uploadFailed": "上传失败", "Done": "完成", "Cancel": "取消", "OK": "确认" @@ -453,7 +468,26 @@ "homepageHeader": "主页", "updateNamespace": "更新名字空间", "removeHomepage": "移除主页", - "selectHomePage": "选择一个页面" + "selectHomePage": "选择一个页面", + "customUrl": "自定义 URL", + "namespace": { + "upgradeToPro": "请升级至 Pro 订阅计划以设置主页", + "redirectToPayment": "正在重定向至付款页面......", + "onlyWorkspaceOwnerCanSetHomePage": "仅工作空间的所有者能为其设置主页", + "pleaseAskOwnerToSetHomePage": "请联系工作空间所有者更新至 Pro 订阅计划" + }, + "publishedPage": { + "title": "所有已发布页面", + "description": "管理您已发布的页面", + "noPublishedPages": "没有已发布的页面", + "settings": "发布设置", + "clickToOpenPageInApp": "在 App 中打开页面", + "clickToOpenPageInBrowser": "在浏览器中打开页面" + }, + "success": { + "setHomepageSuccess": "成功设置主页", + "removeHomePageSuccess": "成功删除主页" + } }, "accountPage": { "menuLabel": "我的账户", @@ -616,10 +650,20 @@ "resetDefault": "重置为默认" }, "resetDialog": { + "title": "重置快捷键", "description": "这将会将所有按键绑定重置为默认,之后无法撤销。你确定要继续吗?" }, + "conflictDialog": { + "confirmLabel": "继续" + }, "keybindings": { - "selectAllCodeblock": "全选" + "selectAllCodeblock": "全选", + "alignLeft": "文本居左对齐", + "alignCenter": "文本居中对齐", + "alignRight": "文本居右对齐", + "undo": "撤销", + "redo": "重做", + "convertToParagraph": "将块转换为段落" } }, "aiPage": { From bbe746c5640f502a180297137881a8e1d17f4e49 Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 12 Feb 2025 15:08:50 +0800 Subject: [PATCH 028/384] feat: support upload svg as icon (#7270) * feat: support upload svg as icon * feat: support upload icon by pasting a link * feat: delete remote images when remove custon icons * chore: add testing for pasting image link as custon icon --------- Co-authored-by: Lucas.Xu --- .../assets/test/images/sample.svg | 4 + .../desktop/sidebar/sidebar_icon_test.dart | 108 +++++++- .../mobile/document/icon_test.dart | 71 +++-- .../shared/common_operations.dart | 39 +++ .../integration_test/shared/emoji.dart | 38 +++ .../integration_test/shared/expectation.dart | 22 +- .../view/database_view_quick_actions.dart | 2 +- .../home/tab/mobile_space_tab.dart | 9 +- .../tab_bar/desktop/tab_bar_header.dart | 2 +- .../lib/plugins/document/document_page.dart | 2 +- .../header/emoji_icon_widget.dart | 139 +++++++--- .../page_style/_page_style_icon_bloc.dart | 2 +- .../lib/shared/appflowy_network_svg.dart | 197 ++++++++++++++ .../flowy_icon_emoji_picker.dart | 1 + .../icon_emoji_picker/icon_uploader.dart | 256 ++++++++++++++---- .../workspace/application/view/view_bloc.dart | 2 +- .../application/view/view_service.dart | 17 +- .../home/menu/view/view_item.dart | 6 +- .../widgets/rename_view_popover.dart | 11 +- .../presentation/widgets/view_title_bar.dart | 2 +- .../packages/flowy_svg/lib/src/flowy_svg.dart | 2 + .../rust-lib/flowy-document/src/entities.rs | 7 + .../flowy-document/src/event_handler.rs | 7 +- .../rust-lib/flowy-document/src/event_map.rs | 2 +- 24 files changed, 802 insertions(+), 146 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/test/images/sample.svg create mode 100644 frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart diff --git a/frontend/appflowy_flutter/assets/test/images/sample.svg b/frontend/appflowy_flutter/assets/test/images/sample.svg new file mode 100644 index 0000000000..7dcd6907d8 --- /dev/null +++ b/frontend/appflowy_flutter/assets/test/images/sample.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index dd40470ab9..2236f03960 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; @@ -7,11 +5,9 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/services.dart'; +import 'package:flowy_svg/flowy_svg.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 '../../shared/base.dart'; import '../../shared/common_operations.dart'; @@ -215,17 +211,12 @@ void main() { } }); - testWidgets('Update page custom icon in title bar', (tester) async { + testWidgets('Update page custom image icon in title bar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); /// prepare local image - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - final iconData = EmojiIconData.custom(imageFile.path); + final iconData = await tester.prepareImageIcon(); // create document, board, grid and calendar views for (final value in ViewLayoutPB.values) { @@ -259,4 +250,97 @@ void main() { ); } }); + + testWidgets('Update page custom svg icon in title bar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// prepare local image + final iconData = await tester.prepareSvgIcon(); + + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + // update its icon + await tester.updatePageIconInTitleBarByName( + name: value.name, + layout: value, + icon: iconData, + ); + + tester.expectViewHasIcon( + value.name, + value, + iconData, + ); + + tester.expectViewTitleHasIcon( + value.name, + value, + iconData, + ); + } + }); + + testWidgets('Update page custom svg icon in title bar by pasting a link', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// prepare local image + const testIconLink = + 'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg'; + + /// create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + /// update its icon + await tester.updatePageIconInTitleBarByPasteALink( + name: value.name, + layout: value, + iconLink: testIconLink, + ); + + /// check if there is a svg in page + final pageName = tester.findPageName( + value.name, + layout: value, + ); + final imageInPage = find.descendant( + of: pageName, + matching: find.byType(SvgPicture), + ); + expect(imageInPage, findsOneWidget); + + /// check if there is a svg in title + final imageInTitle = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.byWidgetPredicate((w) { + if (w is! SvgPicture) return false; + final loader = w.bytesLoader; + if (loader is! SvgFileLoader) return false; + return loader.file.path.endsWith('.svg'); + }), + ); + expect(imageInTitle, findsOneWidget); + } + }); } diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart index 1b94c65436..da7c7e92e7 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart @@ -1,15 +1,12 @@ -import 'dart:io'; - +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/material.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 '../../shared/emoji.dart'; import '../../shared/util.dart'; @@ -18,16 +15,11 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('document title:', () { - testWidgets('update page custom icon in title bar', (tester) async { + testWidgets('update page custom image icon in title bar', (tester) async { await tester.launchInAnonymousMode(); /// prepare local image - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - final iconData = EmojiIconData.custom(imageFile.path); + final iconData = await tester.prepareImageIcon(); /// create an empty page await tester @@ -50,16 +42,63 @@ void main() { /// check result final documentPage = find.byType(MobileDocumentScreen); - final rawEmojiIconWidget = find + final rawEmojiIconFinder = find .descendant( of: documentPage, matching: find.byType(RawEmojiIconWidget), ) - .evaluate() - .first - .widget as RawEmojiIconWidget; + .last; + final rawEmojiIconWidget = + rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; final iconDataInWidget = rawEmojiIconWidget.emoji; expect(iconDataInWidget.type, FlowyIconType.custom); + final imageFinder = + find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image)); + expect(imageFinder, findsOneWidget); + }); + + testWidgets('update page custom svg icon in title bar', (tester) async { + await tester.launchInAnonymousMode(); + + /// prepare local image + final iconData = await tester.prepareSvgIcon(); + + /// create an empty page + await tester + .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey)); + + /// show Page style page + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + final pageStyleIcon = find.byType(PageStyleIcon); + final iconInPageStyleIcon = find.descendant( + of: pageStyleIcon, + matching: find.byType(RawEmojiIconWidget), + ); + expect(iconInPageStyleIcon, findsNothing); + + /// show icon picker + await tester.tapButton(pageStyleIcon); + + /// upload custom icon + await tester.pickImage(iconData); + + /// check result + final documentPage = find.byType(MobileDocumentScreen); + final rawEmojiIconFinder = find + .descendant( + of: documentPage, + matching: find.byType(RawEmojiIconWidget), + ) + .last; + final rawEmojiIconWidget = + rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; + final iconDataInWidget = rawEmojiIconWidget.emoji; + expect(iconDataInWidget.type, FlowyIconType.custom); + final svgFinder = find.descendant( + of: rawEmojiIconFinder, + matching: find.byType(SvgPicture), + ); + expect(svgFinder, findsOneWidget); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 76d2d82b4a..d7da1f4d49 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -50,6 +50,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:universal_platform/universal_platform.dart'; import 'emoji.dart'; @@ -677,6 +679,25 @@ extension CommonOperations on WidgetTester { await pumpAndSettle(); } + Future updatePageIconInTitleBarByPasteALink({ + required String name, + required ViewLayoutPB layout, + required String iconLink, + }) async { + await openPage( + name, + layout: layout, + ); + final title = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.text(name), + ); + await tapButton(title); + await tapButton(find.byType(EmojiPickerButton)); + await pasteImageLinkAsIcon(iconLink); + await pumpAndSettle(); + } + Future openNotificationHub({int tabIndex = 0}) async { final finder = find.descendant( of: find.byType(NotificationButton), @@ -935,6 +956,24 @@ extension CommonOperations on WidgetTester { ), ); } + + Future prepareImageIcon() async { + final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); + final tempDirectory = await getTemporaryDirectory(); + final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); + final imageFile = File(localImagePath) + ..writeAsBytesSync(imagePath.buffer.asUint8List()); + return EmojiIconData.custom(imageFile.path); + } + + Future prepareSvgIcon() async { + final imagePath = await rootBundle.load('assets/test/images/sample.svg'); + final tempDirectory = await getTemporaryDirectory(); + final localImagePath = p.join(tempDirectory.path, 'sample.svg'); + final imageFile = File(localImagePath) + ..writeAsBytesSync(imagePath.buffer.asUint8List()); + return EmojiIconData.custom(imageFile.path); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/integration_test/shared/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart index 41301dd9c1..cccd00a3f6 100644 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/shared/emoji.dart @@ -1,19 +1,24 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; +import 'common_operations.dart'; extension EmojiTestExtension on WidgetTester { Future tapEmoji(String emoji) async { @@ -103,4 +108,37 @@ extension EmojiTestExtension on WidgetTester { ); await tapButton(confirmButton); } + + Future pasteImageLinkAsIcon(String link) async { + final pickTab = find.byType(PickerTab); + expect(pickTab, findsOneWidget); + await pumpAndSettle(); + + /// switch to custom tab + final iconTab = find.descendant( + of: pickTab, + matching: find.text(PickerTabType.custom.tr), + ); + expect(iconTab, findsOneWidget); + await tapButton(iconTab); + + // mock the clipboard + await getIt() + .setData(ClipboardServiceData(plainText: link)); + + // paste the link + await simulateKeyEvent( + LogicalKeyboardKey.keyV, + isControlPressed: Platform.isLinux || Platform.isWindows, + isMetaPressed: Platform.isMacOS, + ); + await pumpAndSettle(const Duration(seconds: 5)); + + /// confirm to upload + final confirmButton = find.descendant( + of: find.byType(IconUploader), + matching: find.byType(PrimaryRoundedButton), + ); + await tapButton(confirmButton); + } } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index 7e9fe4bc38..3b9ef0d75c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; @@ -252,16 +253,19 @@ extension Expectation on WidgetTester { ); expect(icon, findsOneWidget); } else if (type == FlowyIconType.custom) { + final isSvg = data.emoji.endsWith('.svg'); if (isURL(data.emoji)) { final image = find.descendant( of: pageName, - matching: find.byType(FlowyNetworkImage), + matching: isSvg + ? find.byType(FlowyNetworkSvg) + : find.byType(FlowyNetworkImage), ); expect(image, findsOneWidget); } else { final image = find.descendant( of: pageName, - matching: find.byType(Image), + matching: isSvg ? find.byType(SvgPicture) : find.byType(Image), ); expect(image, findsOneWidget); } @@ -290,16 +294,26 @@ extension Expectation on WidgetTester { ); expect(icon, findsOneWidget); } else if (type == FlowyIconType.custom) { + final isSvg = data.emoji.endsWith('.svg'); if (isURL(data.emoji)) { final image = find.descendant( of: find.byType(ViewTitleBar), - matching: find.byType(FlowyNetworkImage), + matching: isSvg + ? find.byType(FlowyNetworkSvg) + : find.byType(FlowyNetworkImage), ); expect(image, findsOneWidget); } else { final image = find.descendant( of: find.byType(ViewTitleBar), - matching: find.byType(Image), + matching: isSvg + ? find.byWidgetPredicate((w) { + if (w is! SvgPicture) return false; + final loader = w.bytesLoader; + if (loader is! SvgFileLoader) return false; + return loader.file.path.endsWith('.svg'); + }) + : find.byType(Image), ); expect(image, findsOneWidget); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart index 505e012f7b..a133739a9d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -75,7 +75,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { enableBackgroundColorSelection: false, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: r.data, ); Navigator.pop(context); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index cba6f0fd9a..b819757111 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -73,7 +73,14 @@ class _MobileSpaceTabState extends State listener: (context, state) { final lastCreatedPage = state.lastCreatedPage; if (lastCreatedPage != null) { - context.pushView(lastCreatedPage); + context.pushView( + lastCreatedPage, + tabs: [ + PickerTabType.emoji, + PickerTabType.icon, + PickerTabType.custom, + ].map((e) => e.name).toList(), + ); } }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index c7cc314046..ea69f40540 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -263,7 +263,7 @@ class _TabBarItemButtonState extends State { enableBackgroundColorSelection: false, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 561d281154..8d01e1ae12 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -252,7 +252,7 @@ class _DocumentPageState extends State editorState: editorState, view: widget.view, onIconChanged: (icon) async => ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: icon, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 733454d021..99b50c49c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -3,9 +3,12 @@ import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; import 'package:string_validator/string_validator.dart'; @@ -62,7 +65,7 @@ class _EmojiIconWidgetState extends State { } } -class RawEmojiIconWidget extends StatelessWidget { +class RawEmojiIconWidget extends StatefulWidget { const RawEmojiIconWidget({ super.key, required this.emoji, @@ -74,64 +77,92 @@ class RawEmojiIconWidget extends StatelessWidget { final double emojiSize; final bool enableColor; + @override + State createState() => _RawEmojiIconWidgetState(); +} + +class _RawEmojiIconWidgetState extends State { + UserProfilePB? userProfile; + + EmojiIconData get emoji => widget.emoji; + + @override + void initState() { + super.initState(); + loadUserProfile(); + } + + @override + void didUpdateWidget(RawEmojiIconWidget oldWidget) { + super.didUpdateWidget(oldWidget); + loadUserProfile(); + } + @override Widget build(BuildContext context) { final defaultEmoji = SizedBox( - width: emojiSize, + width: widget.emojiSize, child: EmojiText( emoji: '❓', - fontSize: emojiSize, + fontSize: widget.emojiSize, textAlign: TextAlign.center, ), ); try { - switch (emoji.type) { + switch (widget.emoji.type) { case FlowyIconType.emoji: return SizedBox( - width: emojiSize, + width: widget.emojiSize, child: EmojiText( - emoji: emoji.emoji, - fontSize: emojiSize, + emoji: widget.emoji.emoji, + fontSize: widget.emojiSize, textAlign: TextAlign.justify, ), ); case FlowyIconType.icon: - IconsData iconData = IconsData.fromJson(jsonDecode(emoji.emoji)); - if (!enableColor) { + IconsData iconData = + IconsData.fromJson(jsonDecode(widget.emoji.emoji)); + if (!widget.enableColor) { iconData = iconData.noColor(); } /// Under the same width conditions, icons on macOS seem to appear /// larger than emojis, so 0.9 is used here to slightly reduce the /// size of the icons - final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize; + final iconSize = + Platform.isMacOS ? widget.emojiSize * 0.9 : widget.emojiSize; return IconWidget( iconsData: iconData, size: iconSize, ); case FlowyIconType.custom: - final url = emoji.emoji; + final url = widget.emoji.emoji; + final isSvg = url.endsWith('.svg'); + final hasUserProfile = userProfile != null; if (isURL(url)) { - return SizedBox.square( - dimension: emojiSize, - child: FutureBuilder( - future: UserBackendService.getCurrentUserProfile(), - builder: (context, value) { - final userProfile = value.data?.fold( - (userProfile) => userProfile, - (l) => null, - ); - if (userProfile == null) return const SizedBox.shrink(); - return FlowyNetworkImage( - url: url, - width: emojiSize, - height: emojiSize, - userProfilePB: userProfile, - errorWidgetBuilder: (context, url, error) => - const SizedBox.shrink(), - ); + Widget child = const SizedBox.shrink(); + if (isSvg) { + child = FlowyNetworkSvg( + url, + headers: + hasUserProfile ? _buildRequestHeader(userProfile!) : {}, + width: widget.emojiSize, + height: widget.emojiSize, + ); + } else if (hasUserProfile) { + child = FlowyNetworkImage( + url: url, + width: widget.emojiSize, + height: widget.emojiSize, + userProfilePB: userProfile, + errorWidgetBuilder: (context, url, error) { + return const SizedBox.shrink(); }, - ), + ); + } + return SizedBox.square( + dimension: widget.emojiSize, + child: child, ); } final imageFile = File(url); @@ -139,13 +170,19 @@ class RawEmojiIconWidget extends StatelessWidget { throw PathNotFoundException(url, const OSError()); } return SizedBox.square( - dimension: emojiSize, - child: Image.file( - imageFile, - fit: BoxFit.cover, - width: emojiSize, - height: emojiSize, - ), + dimension: widget.emojiSize, + child: isSvg + ? SvgPicture.file( + File(url), + width: widget.emojiSize, + height: widget.emojiSize, + ) + : Image.file( + imageFile, + fit: BoxFit.cover, + width: widget.emojiSize, + height: widget.emojiSize, + ), ); } } catch (e) { @@ -153,4 +190,32 @@ class RawEmojiIconWidget extends StatelessWidget { return defaultEmoji; } } + + Map _buildRequestHeader(UserProfilePB userProfilePB) { + final header = {}; + final token = userProfilePB.token; + try { + final decodedToken = jsonDecode(token); + header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; + } catch (e) { + Log.error('Unable to decode token: $e'); + } + return header; + } + + Future loadUserProfile() async { + if (userProfile != null) return; + if (emoji.type == FlowyIconType.custom) { + final userProfile = + (await UserBackendService.getCurrentUserProfile()).fold( + (userProfile) => userProfile, + (l) => null, + ); + if (mounted) { + setState(() { + this.userProfile = userProfile; + }); + } + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart index 2578b6de07..b2cd77f312 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart @@ -37,7 +37,7 @@ class PageStyleIconBloc extends Bloc { emit(state.copyWith(icon: icon)); if (shouldUpdateRemote && icon != null) { await ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: icon, ); } diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart new file mode 100644 index 0000000000..33c3bb2c0a --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart @@ -0,0 +1,197 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:flowy_svg/flowy_svg.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +import 'custom_image_cache_manager.dart'; + +class FlowyNetworkSvg extends StatefulWidget { + FlowyNetworkSvg( + this.url, { + Key? key, + this.cacheKey, + this.placeholder, + this.errorWidget, + this.width, + this.height, + this.headers, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.matchTextDirection = false, + this.allowDrawingOutsideViewBox = false, + this.semanticsLabel, + this.excludeFromSemantics = false, + this.theme = const SvgTheme(), + this.fadeDuration = Duration.zero, + this.colorFilter, + this.placeholderBuilder, + BaseCacheManager? cacheManager, + }) : cacheManager = cacheManager ?? CustomImageCacheManager(), + super(key: key ?? ValueKey(url)); + + final String url; + final String? cacheKey; + final Widget? placeholder; + final Widget? errorWidget; + final double? width; + final double? height; + final ColorFilter? colorFilter; + final Map? headers; + final BoxFit fit; + final AlignmentGeometry alignment; + final bool matchTextDirection; + final bool allowDrawingOutsideViewBox; + final String? semanticsLabel; + final bool excludeFromSemantics; + final SvgTheme theme; + final Duration fadeDuration; + final WidgetBuilder? placeholderBuilder; + final BaseCacheManager cacheManager; + + @override + State createState() => _FlowyNetworkSvgState(); + + static Future preCache( + String imageUrl, { + String? cacheKey, + BaseCacheManager? cacheManager, + }) { + final key = cacheKey ?? _generateKeyFromUrl(imageUrl); + cacheManager ??= DefaultCacheManager(); + return cacheManager.downloadFile(key); + } + + static Future clearCacheForUrl( + String imageUrl, { + String? cacheKey, + BaseCacheManager? cacheManager, + }) { + final key = cacheKey ?? _generateKeyFromUrl(imageUrl); + cacheManager ??= DefaultCacheManager(); + return cacheManager.removeFile(key); + } + + static Future clearCache({BaseCacheManager? cacheManager}) { + cacheManager ??= DefaultCacheManager(); + return cacheManager.emptyCache(); + } + + static String _generateKeyFromUrl(String url) => url.split('?').first; +} + +class _FlowyNetworkSvgState extends State + with SingleTickerProviderStateMixin { + bool _isLoading = false; + bool _isError = false; + File? _imageFile; + late String _cacheKey; + + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _cacheKey = + widget.cacheKey ?? FlowyNetworkSvg._generateKeyFromUrl(widget.url); + _controller = AnimationController( + vsync: this, + duration: widget.fadeDuration, + ); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); + _loadImage(); + } + + Future _loadImage() async { + try { + _setToLoadingAfter15MsIfNeeded(); + + var file = (await widget.cacheManager.getFileFromMemory(_cacheKey))?.file; + + file ??= await widget.cacheManager.getSingleFile( + widget.url, + key: _cacheKey, + headers: widget.headers ?? {}, + ); + + _imageFile = file; + _isLoading = false; + + _setState(); + + await _controller.forward(); + } catch (e) { + log('CachedNetworkSVGImage: $e'); + + _isError = true; + _isLoading = false; + + _setState(); + } + } + + void _setToLoadingAfter15MsIfNeeded() => Future.delayed( + const Duration(milliseconds: 15), + () { + if (!_isLoading && _imageFile == null && !_isError) { + _isLoading = true; + _setState(); + } + }, + ); + + void _setState() => mounted ? setState(() {}) : null; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: widget.height, + child: _buildImage(), + ); + } + + Widget _buildImage() { + if (_isLoading) return _buildPlaceholderWidget(); + + if (_isError) return _buildErrorWidget(); + + return FadeTransition( + opacity: _animation, + child: _buildSVGImage(), + ); + } + + Widget _buildPlaceholderWidget() => + Center(child: widget.placeholder ?? const SizedBox()); + + Widget _buildErrorWidget() => + Center(child: widget.errorWidget ?? const SizedBox()); + + Widget _buildSVGImage() { + if (_imageFile == null) return const SizedBox(); + + return SvgPicture.file( + _imageFile!, + fit: widget.fit, + width: widget.width, + height: widget.height, + alignment: widget.alignment, + matchTextDirection: widget.matchTextDirection, + allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox, + colorFilter: widget.colorFilter, + semanticsLabel: widget.semanticsLabel, + excludeFromSemantics: widget.excludeFromSemantics, + placeholderBuilder: widget.placeholderBuilder, + theme: widget.theme, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart index 137ecbf94d..b04b38a45a 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart @@ -243,6 +243,7 @@ class _FlowyIconEmojiPickerState extends State Widget _buildIconUploader() { return IconUploader( documentId: widget.documentId ?? '', + ensureFocus: true, onUrl: (url) { widget.onSelectedEmoji ?.call(SelectedEmojiIconResult(EmojiIconData.custom(url), false)); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index 9ebcd78def..9bc47e35db 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -1,7 +1,11 @@ import 'dart:io'; 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/plugins/document/presentation/editor_plugins/image/image_util.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; +import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/shared/patterns/file_type_patterns.dart'; import 'package:appflowy/shared/permission/permission_checker.dart'; import 'package:appflowy/startup/startup.dart'; @@ -15,8 +19,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:string_validator/string_validator.dart'; import 'package:universal_platform/universal_platform.dart'; @visibleForTesting @@ -25,10 +32,12 @@ class IconUploader extends StatefulWidget { super.key, required this.onUrl, required this.documentId, + this.ensureFocus = false, }); final ValueChanged onUrl; final String documentId; + final bool ensureFocus; @override State createState() => _IconUploaderState(); @@ -38,53 +47,97 @@ class _IconUploaderState extends State { bool isHovering = false; bool isUploading = false; - final List pickedImages = []; + final List<_Image> pickedImages = []; + final FocusNode focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time + if (widget.ensureFocus) { + Future.delayed(const Duration(milliseconds: 200), () { + if (!mounted || focusNode.hasFocus) return; + focusNode.requestFocus(); + }); + } + } + + @override + void dispose() { + super.dispose(); + focusNode.dispose(); + } @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Expanded( - child: DropTarget( - /// there is an issue with multiple DropTargets - /// see https://github.com/MixinNetwork/flutter-plugins/issues/2 - enable: false, - onDragEntered: (_) => setState(() => isHovering = true), - onDragExited: (_) => setState(() => isHovering = false), - onDragDone: (details) => loadImage(details.files), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => pickImage(), - child: DottedBorder( - dashPattern: const [3, 3], - radius: const Radius.circular(8), - borderType: BorderType.RRect, - color: isHovering - ? Theme.of(context).colorScheme.primary - : Theme.of(context).hintColor, - child: Center( - child: pickedImages.isEmpty ? dragHint() : previewImage(), + return Shortcuts( + shortcuts: { + LogicalKeySet( + Platform.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.keyV, + ): _PasteIntent(), + }, + child: Actions( + actions: { + _PasteIntent: CallbackAction<_PasteIntent>( + onInvoke: (intent) => pasteAsAnImage(), + ), + }, + child: Focus( + autofocus: true, + focusNode: focusNode, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: DropTarget( + /// there is an issue with multiple DropTargets + /// see https://github.com/MixinNetwork/flutter-plugins/issues/2 + enable: false, + onDragEntered: (_) => setState(() => isHovering = true), + onDragExited: (_) => setState(() => isHovering = false), + onDragDone: (details) => loadImage(details.files), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => pickImage(), + child: DottedBorder( + dashPattern: const [3, 3], + radius: const Radius.circular(8), + borderType: BorderType.RRect, + color: isHovering + ? Theme.of(context).colorScheme.primary + : Theme.of(context).hintColor, + child: Center( + child: pickedImages.isEmpty + ? dragHint() + : previewImage(), + ), + ), + ), ), ), ), - ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Align( + alignment: Alignment.centerRight, + child: _ConfirmButton( + onTap: uploadImage, + enable: pickedImages.isNotEmpty, + ), + ), + ), + ], ), ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Align( - alignment: Alignment.centerRight, - child: _ConfirmButton( - onTap: uploadImage, - enable: pickedImages.isNotEmpty, - ), - ), - ), - ], + ), ), ); } @@ -96,26 +149,54 @@ class _IconUploaderState extends State { color: Theme.of(context).hintColor, ); - Widget previewImage() => Image.file( - File(pickedImages.first), + Widget previewImage() { + final image = pickedImages.first; + final url = image.url; + if (image is _FileImage) { + if (url.endsWith(_svgSuffix)) { + return SvgPicture.file( + File(url), + width: 200, + height: 200, + ); + } + return Image.file( + File(url), width: 200, height: 200, - fit: BoxFit.cover, ); + } else if (image is _NetworkImage) { + if (url.endsWith(_svgSuffix)) { + return FlowyNetworkSvg( + url, + width: 200, + height: 200, + ); + } + return FlowyNetworkImage( + width: 200, + height: 200, + url: url, + ); + } + return const SizedBox.shrink(); + } void loadImage(List files) { final imageFiles = files .where( (file) => file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), + false || + imgExtensionRegex.hasMatch(file.name) || + file.name.endsWith(_svgSuffix), ) .toList(); if (imageFiles.isEmpty) return; if (mounted) { setState(() { pickedImages.clear(); - pickedImages.add(imageFiles.first.path); + pickedImages.add(_FileImage(imageFiles.first.path)); }); } } @@ -126,7 +207,7 @@ class _IconUploaderState extends State { final result = await getIt().pickFiles( dialogTitle: '', type: FileType.custom, - allowedExtensions: defaultImageExtensions, + allowedExtensions: List.of(defaultImageExtensions)..add('svg'), ); loadImage(result?.files.map((f) => f.xFile).toList() ?? const []); } else { @@ -154,22 +235,27 @@ class _IconUploaderState extends State { final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; if (isLocalMode) { - result = await saveImageToLocalStorage(pickedImages.first); + result = await pickedImages.first.saveToLocal(); } else { - final (url, errorMsg) = await saveImageToCloudStorage( - pickedImages.first, - widget.documentId, - ); - result = url; - if (errorMsg?.isNotEmpty ?? false) { - Log.error('upload icon image error :$errorMsg'); - } + result = await pickedImages.first.uploadToCloud(widget.documentId); } isUploading = false; if (result?.isNotEmpty ?? false) { widget.onUrl.call(result!); } } + + Future pasteAsAnImage() async { + final data = await getIt().getData(); + final plainText = data.plainText; + Log.info('pasteAsAnImage plainText:$plainText'); + if (isURL(plainText)) { + setState(() { + pickedImages.clear(); + pickedImages.add(_NetworkImage(plainText!)); + }); + } + } } class _ConfirmButton extends StatelessWidget { @@ -192,3 +278,65 @@ class _ConfirmButton extends StatelessWidget { ); } } + +const _svgSuffix = '.svg'; + +class _PasteIntent extends Intent {} + +abstract class _Image { + String get url; + + Future saveToLocal(); + + Future uploadToCloud(String documentId); + + String get pureUrl => url.split('?').first; +} + +class _FileImage extends _Image { + _FileImage(this.url); + + @override + final String url; + + @override + Future saveToLocal() => saveImageToLocalStorage(url); + + @override + Future uploadToCloud(String documentId) async { + final (url, errorMsg) = await saveImageToCloudStorage( + this.url, + documentId, + ); + if (errorMsg?.isNotEmpty ?? false) { + Log.error('upload icon image :${this.url} error :$errorMsg'); + } + return url; + } +} + +class _NetworkImage extends _Image { + _NetworkImage(this.url); + + @override + final String url; + + @override + Future saveToLocal() async { + final file = await CustomImageCacheManager().downloadFile(pureUrl); + return file.file.path; + } + + @override + Future uploadToCloud(String documentId) async { + final file = await CustomImageCacheManager().downloadFile(pureUrl); + final (url, errorMsg) = await saveImageToCloudStorage( + file.file.path, + documentId, + ); + if (errorMsg?.isNotEmpty ?? false) { + Log.error('upload icon image :${this.url} error :$errorMsg'); + } + return url; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 2a4c472397..7c2a4d9b64 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -262,7 +262,7 @@ class ViewBloc extends Bloc { }, updateIcon: (value) async { await ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: view.icon.toEmojiIconData(), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 6599db1273..709515f1b3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -4,6 +4,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -190,14 +192,25 @@ class ViewBackendService { } static Future> updateViewIcon({ - required String viewId, + required ViewPB view, required EmojiIconData viewIcon, }) { + final viewId = view.id; + final oldIcon = view.icon.toEmojiIconData(); final icon = viewIcon.toViewIcon(); final payload = UpdateViewIconPayloadPB.create() ..viewId = viewId ..icon = icon; - + if (oldIcon.type == FlowyIconType.custom && + viewIcon.emoji != oldIcon.emoji) { + DocumentEventDeleteFile( + DeleteFilePB(url: oldIcon.emoji), + ).send().onFailure((e) { + Log.error( + 'updateViewIcon error while deleting :${oldIcon.emoji}, error: ${e.msg}, ${e.code}', + ); + }); + } return FolderEventUpdateViewIcon(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 931d8d1a33..ae9059c623 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -616,7 +616,7 @@ class _SingleInnerViewItemState extends State { offset: const Offset(0, 5), direction: PopoverDirection.bottomWithLeftAligned, popupBuilder: (_) => RenameViewPopover( - viewId: widget.view.id, + view: widget.view, name: widget.view.name, emoji: widget.view.icon.toEmojiIconData(), popoverController: popoverController, @@ -662,7 +662,7 @@ class _SingleInnerViewItemState extends State { documentId: widget.view.id, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) controller.close(); @@ -801,7 +801,7 @@ class _SingleInnerViewItemState extends State { return; } await ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: data.data, ); break; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index 8e339ca17c..954fc77603 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -12,7 +13,7 @@ import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ super.key, - required this.viewId, + required this.view, required this.name, required this.popoverController, required this.emoji, @@ -21,7 +22,7 @@ class RenameViewPopover extends StatefulWidget { this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final String viewId; + final ViewPB view; final String name; final PopoverController popoverController; final EmojiIconData emoji; @@ -64,7 +65,7 @@ class _RenameViewPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), onSubmitted: _updateViewIcon, - documentId: widget.viewId, + documentId: widget.view.id, tabs: widget.tabs, ), ), @@ -88,7 +89,7 @@ class _RenameViewPopoverState extends State { Future _updateViewName(String name) async { if (name.isNotEmpty && name != widget.name) { await ViewBackendService.updateView( - viewId: widget.viewId, + viewId: widget.view.id, name: _controller.text, ); widget.popoverController.close(); @@ -100,7 +101,7 @@ class _RenameViewPopoverState extends State { PopoverController? _, ) async { await ViewBackendService.updateViewIcon( - viewId: widget.viewId, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 1a1c993248..a1f2717d97 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -315,7 +315,7 @@ class _ViewTitleState extends State { // icon + textfield _resetTextEditingController(state); return RenameViewPopover( - viewId: widget.view.id, + view: widget.view, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), diff --git a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart index b5ded7b713..1f861156eb 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +export 'package:flutter_svg/flutter_svg.dart'; + /// The class for FlowySvgData that the code generator will implement class FlowySvgData { /// The svg data diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index 24611a8140..74157c6124 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -115,6 +115,13 @@ pub struct DownloadFilePB { pub local_file_path: String, } +#[derive(Default, ProtoBuf, Validate)] +pub struct DeleteFilePB { + #[pb(index = 1)] + #[validate(url)] + pub url: String, +} + #[derive(Default, ProtoBuf)] pub struct CreateDocumentPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 774561cc4e..387e216f08 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -485,13 +485,10 @@ pub(crate) async fn download_file_handler( // Handler for deleting file pub(crate) async fn delete_file_handler( - params: AFPluginData, + params: AFPluginData, manager: AFPluginState>, ) -> FlowyResult<()> { - let DownloadFilePB { - url, - local_file_path: _, - } = params.try_into_inner()?; + let DeleteFilePB { url } = params.try_into_inner()?; let manager = upgrade_document(manager)?; manager.delete_file(url).await } diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index e05519d81e..1931d32161 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -126,7 +126,7 @@ pub enum DocumentEvent { UploadFile = 15, #[event(input = "DownloadFilePB")] DownloadFile = 16, - #[event(input = "DownloadFilePB")] + #[event(input = "DeleteFilePB")] DeleteFile = 17, #[event(input = "UpdateDocumentAwarenessStatePB")] From 4a7e20b3a57131e16409cf7cee1ab1245caa3474 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 12 Feb 2025 17:35:59 +0800 Subject: [PATCH 029/384] fix: save image should not copy the image (#7370) * fix: save image should not copy the image * fix: unable to scroll the table in AI Chat on mobile --- .../android/app/src/main/AndroidManifest.xml | 4 +- frontend/appflowy_flutter/ios/Podfile.lock | 6 + .../appflowy_flutter/ios/Runner/Info.plist | 143 +++++++++--------- .../message/ai_markdown_text.dart | 3 +- .../presentation/message/ai_text_message.dart | 6 +- .../custom_image_block_component.dart | 35 ++++- .../simple_table/simple_table_constants.dart | 2 +- .../shared/permission/permission_checker.dart | 4 +- frontend/appflowy_flutter/pubspec.lock | 12 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- 10 files changed, 129 insertions(+), 89 deletions(-) diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index fe9744ef8f..284ce0ffd7 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -46,6 +46,8 @@ + PUsQ+{5+}F*o^3U1mxUrVzjhA0^Ul#@3rRBMoj4h9P zL48+5Fy(Sp70@YJTdd!Ox<9Nzro3JsLfWDA6_b)a{ON(-S#S6zA_Zcu5HW-gZCWAU1XQ_Rm2g7q%@cz>wkzh^D7@LXqlnagK%f3{TJmB-xYr zpb(@iIr(e6fH9MAqN(BC5x``yVEVU}ecC5dL?U%sKDmA^(y_(Pr&%I8ajC~eXja0C&FWI*z%y16idyxEzC$E2p_j-J;mQeCkk3 zPDcQ*; zB6XPUVy79)ko&_G3r?McfFAl`$+wuw&X0DwTOPdxhoW7Win2x3Bk<@m@GgwsQ%!h0 z4Ehl&(DWxRS=!~p*e@AJ0CXbx%;J0JvreOWd*!`JLfcS>B|XT3p5@7Qc#NFT5x~RP z=Zoi>VR(ZsX+?PxDgP4vAqibjT=TU@9d1OYS~*MMrqd_V z3`{c`Q!+v=^1sa)fn_`nHyhIrXt`CA#QmvFyZKYa#Ok3_2uC}!jzTx8#?G@HcdLLU zKDD#lCBE|`Ax<8Hfz#eglK8jw4SpZI%v~Dz0zv2xTsLup%kTq_gum4cz6f zx^dU;i!t-Udi#8bC1lN@v!wC>b={{>_~&bx-`&nw>S~9ZiY)B)!?gTvDvXtlls27v z^7X^b5nTIKM%HIBeIKmS2|1*FLkcC2KWaULq?N8A^BYxFXf6~^vMA@INKv5FN@@Q! zvC?~moWyiki+Oe%1hF*_#Z{2L=HY15Q-20E)2Fe^!7nhkzPBU3 z%I&2TBE?*Z<8ldZ)O7WwNXqr>#O8U4kle0z+cCv0er1%j`c`wdW+mu$z_ntg4{)Y| z5NLechv*H=2~8TCh=>i=>&>3?LN zIuC!?e8`;Lehr^)49va%aITCEZu^5b|-f+ehZO|$I}(IWt7_i@0rCBZfJeT=Jq zjhpqetTfhm-D}m8a~+Y-XR$Ra8-f$N&AFl21s1P@YJ34A>)AVztYv)&ei-u-ogRsXm;!P9GH@t3iXCfExT zW$h6IpNY01Rl5DS+XD~d<<6IpPj!VKPFc9s4%Q;H-=rjd?M<3@v2=#;DA-lqwSe-D z41U~-rKRW?1{&LVvLg~UTxvtdD|(}x+r;{!uI@mEr9@07927dkN_hX;h_Vsp5Tmwd z@HAdf(!x$c{P?}u?wUGcfZ#YMzyes4A|J9AVs0}11Q!h)a4K@96QARd`8+@~Qc$=G=p^JjPY-$)Vh}A}y;WweBB<<< zn=Eed=EvSk_j<0;2ajjojbX|ph{dG8x6zNU%8YDy;Q!mFI4Pgq zTFdLg>SaDz0rTBn#oBf?!@#I4Mdu+Si)2-7v5Z|-W;7ogB`b(_jFqz&m8_g|_xVDV z{W@Fr5q-CZKlZJ9k0>r!x2HGIWnTp)t4hC9pSTd1_~DR*noXu7?qo!^9}b4qYZG!h zHL#(GpEuM4JXyYaD)Qu}=RlMza3Af%V*9N*!tYTh+vcBd zw1wt?^=*b6Gs7+m*=WeJGubxbB4unaoi#WU$8_7xnKE5^KcN8?p_{8LIjP#Z4#}>C zGWTs(I6bkEHxxa)O+ht&Qc~D2UrJ#k*2)96YE#jf(Y^kto?G``X-)*j^X)6cOLeuv zGbT!A*su=>wrr7^tV+MpW_se%H1{UQH12U#bz=%$EokA;(8oFrhWH3- za*}HP8XiJ%EA4itL3;{xt-ii`mPP=PBWKx1fR%8A(<^&t_GTj-pAQ>^3I~=+K*V*C+c_)(H3&U?j z$nK3L71e7B_HqsKiydq$iMdg{IXjkU*pzhMMz(D@zj*#Yk^iaZE3LK_FzYHOlTj6o zA^I}<$(R2BAV{@hFjI`w{nu%aI$i*-2IsVB<4sts+;?2qnjkJ6aL zCl2b$j3r<27%d%r?w#<_I3^DE;2-_cN;@@q6AmgNgdpjB~-I6I)jxRm^5SQ`jY#Ds(>}r>-n@@_e=f=kC6; zd+z7-4NCe~!TS*}L%1mnL3S0zyTysbIU8)2=0BhvPKV;kL2AL1872tFgo3KIr>^1- zj5{+`)W{`VLK~H&(ywxl;UR9F2aSo6lxBj8-&BS3f0Ju<$w)i-rYo;2`$8d5?opp+ z>w@r&ad^jv`bLhut3WUWR*yzXx}NWPo_m%t`8wlSghBx4281|`BwaOfPq)9H8@%Yq zvZ%RSPxAVlPEQ2d-T(2`ys^#mRJz8%y>o-0B|G+YmN|*qgOc<;XlDEvwynE#sM}Tf zVM-Re(*7)Zb*j|QD&rE={!P)7<@Ej@+=o1*u27;4B42+irpnvM$Mcsr%GzS3Hg9~s zwV|VWM<~f!TyT3-O9=}zaP``r_Cy(Kn5v)ck<~k$hUpD3u(J#JTuTRi7+=P2XC*;w zOV+Yq0EM_#qmRPWVDxD#>4L#W-=1haq|X&1)M{%mHG2Lt)T)A3b4CQM^KD znn8ZIJsnR(nuFja-8H2-B9AgOCW5ixpBk05zvmRsuunvADX?4@qp{MhLlPCAsu}J0 zV%RrGURWLNQGuBZf_Fb9gks8Ye)=O)tExkVCe;|QPg6pvjcN7%DDd&+S8X`It95=K z!!i~_T5U=sm$q%471%;&LHbpTF5^ScA-u$f?)mni3W#4tL@xilR)%jx<} z3(bD$*+bNul`(dikBfKX<|G1CHN`GAF5Z|@g`N)!fIIeY$8TQNd%J!c@~$}N0c^gD zUSYE8;&-`1Q7&jItCMio>dm6|wIe{)YBH%+NGGH)`~!W`^R|rk9~I-;abF8NQsmlB z;_?eH$IE4361)v|fL+eAQY#@;xkV|hNASjA?m4Gk{JR#k`gzcvWXof2LEdiUA|KZ> zECausJ)Iyt&_4plCx74Csos;f?~8IrN;B40aiX>YKi!+K?@Ub5)$(m&DuNPuve?#P z_ShW(hD%@TeRVAx!K#M2=uhTj6{?zD@Q>fyY&W#Lp}@57RRwZ~=?@3@oD9o81w!ZCjL${Y=zZ*b4!+o((tCTr+mbNLz%t7IdO4%rm*DGgi%KQ(WVV_&1G1;*|M zOd`xDhgWS4tG^O%UMbr1)l=1~s=2u{{sjJC-;E?Y`0l{adPD!s)}cUCWCJFs{~^f% z5g_r=D%SWMwIfy}`;wK-WdO z9p+Kwmm->>);T{ptADZU7m?_c|p@Dw!Ot zyx20WZbjihN(!o=v<&)o1n8VTyEFeyn7;%0W)iJob}IK1ulm{&@e4~h@ph%(dK9_P znKLxs*hkJ4a^PTDG+k#}J`8<1hG@{^0aD)M|w>eonX?HE7rFS#_#XmHo{#qkMz z+Hz+9vi0gcn*Vm?-PAtQ97h`@HZSo#+E4O(c*b>o=>6RM;;y`p;+&FxN(#fUn3Y(cSzP9b*KB*;rGc6^SXQBQ%e7#-= zg3TE@EbSsh!z^CpJ5hb}Aa`s#W+SPrA=0#P*xLr(KZ4_iG)HxI`n;CA(rpaQ#iPsK z&8ZN7K~O$IuaihOOj+lwBY&TK*tN*l9@t`fK<`_7sTw8L zP~$p}+rfx^!%6e^7CY-DP0xnQfslqO9DkoR;u)-u=5APvfYNTo4$rY#>$Y1`G8UE5 zFY~_PVVE0LiL|?2gI5Vf4n&2`JK@+G-v_lM!%>If9Y|8>)hhN_x5JFaP=!>Uj*<^p zgc;}kZpt_bQ{=4(t9>0g6HUd&FMCyR4qPd=>+k9p^`xk<%0}b;%pgZr1cn zXvUf=PRCIc`ic3R^}ZFpsB5NwT_msPLF?j`OGqW>jJI2J{VB3GLolcTY2H!Q{0MOU z;K4#5d4wn`HZVhqNo;dR7o}&_86pm2DWkD<6dNpNDkmY$9%>U+5Ssm6 zb&Ip^P6kcJ29B2s#m%Z}UIH7Aw;HI*RCQ(iWNb!E5upf#V{usNo8{*3QHF1d zrG1z9MR>DTTKBsiQTbAHjGg^@aUzUQNJTzJIP z!&~>`Fhisf8x^bdNHWThP}rX#G8*m}>2h0TW=xFrysvh#K#1Z`|@7&u)o~nyCHFkk;vlB2^09Ca7GGZ}Bm{7`wDM3(ezkJ0xhig5q&3iDsoo6AuH{sNr>pV&7i>Qrl z=CvUAaKVYV1_>K|P}QAZ>(q+3Nt;XkO9@yt(he;S7qm(j6@IJI+T7|qvbx_^Cklmn zd=h*BSH+ht2|C8aYaX1#sBq1s$Se9Aka-`AiefXjC*Ev!=kegmlGJbnXgD-Pa>af= zB^RbPbOcyhoHIvxQ<;Q=@jZdFd6%vZ@-&5HUT7uaJ5N>G1FKS*HAp0GMLG;&%cm@I)PBl}yP{1!=gpvdeAa(5TPJ0d@6Tg* ze|=crPBdvND8k;4+q7S1>nmy&2$yhcNvnA@le-_f_%-MPB+#=}WlzVUYdd3EC|ERO zc@}K=p?{ew`27grAB&DY?Q=irw6t{S)~9u7@lN2!FuvhNl`BU8y1c@u7giTuvXZ_J z#C#CAm%nKHy=qeWPbSssQR0`xyzCVBKYUByFOWUVR}|-dd9JcJ))q@JjWaEx z8tMIf&`VYc=M{iQ6v7%3Ct9z_Pkk=>R6Yg_BW;YsOrx@2a4U_=c=%HBz+Sz?C)FrF zbEJ{jIru%cjA>-Y;R7+pF^-r22R|oIXbti?Gl$9JY!WTLtd$r0V`J*d7g*M#(O^4G zTJFe#<+Fj7srR(>@6K1XqNM4%9}a$2h@m1*aHYeeX+tTXkE}ESF)K{9sWy{XvNp)p{({ z2;CbpwXd^~RpY1Rj~V)y;xKAoI}~)q|4D*5s4w9KE&EdJ9UBuz*uK&l=uw=Uu;%*G zUTK8(P6=bPXv0*mJ1!qzO6XKgNOB))xFS;nXTK)01&b{<8Wq_un(5zXTI?gllD z7Va!R?X2UHYzqT)>Uf@N3a@uxJ*|CfRy0>sCUA{+#~|;+A-E4&zHB$PCs`y?yn+Pd zVEmx;Pu0G*sSbxF#+sijt6btqtlM`#{#y9b-mb&rBLy3OwwPdjuH3MglI)!0P;zm$ zwK;D0@z|cZLz&~-u17$h1_K$%J&D~52}Pd45*x#obA7)N` zYhg>VO7#?Ow68LAOaroO;zTH*Bq?{eKU*;{j!CVPI)^ZlsPZ9`)GZG+kz-^dx4jMe9=Q@IL#&&MHg$*h6M z>laPvoqf-9(t3;PqJnXb4*O$EFH?t!D7Pk|i8_=d`b@JQ$#75>cTJ~s*R%QSQe#S5 z^Ml%erpp+Q06G5n53g2&BcroyCSMiI<#r#&gN&Rl2t!48dcF6eM8Q!CstqEEyTzJu zB#$j3?=CxWY=c_Hm6k_l`1WA+y5t8o*8_{@P-hN2-!H)VGUvivfOCy5 z(UUXUFMY?{Hm}EDTdZ*q*HImWFov{h4r~{{Hcm^?-xWJGoQvD+H_uthky-6P?Dib5 zR9BK^2=I1)@LzRSrK3LLAC%#rj z)xFkLIpop1)L2OfTWq@>ZkW7SY~LpQ^e_Kh7m;gkD~FXBw#Z1;0V5g^lH*GONdj+d zcsq!D908CRPWCvZ--PhpP^gYCX&*fR_LI6(Z|?7BcG(n?nY1kr9fotCD(Gk<0+-lD zULvRLzhC$cf-O&Ztm^*Nq49)47l*|gDv2u=8yP**EhgAT)841juY57Gp@~Aw;3IR# zJD#eK0Iv`9Hl_tO&@S?e+k_!searV>XAfsOzcZ+6VxwFB&SQ}G{*0Mk6U!)d$m6!V zW`Zu)9OOZVE*$iRxeUu0+DRBPyO=;(Xk2b_cFNTf!ecOHHFllZ_gt|x;P{l6qe<5$ zJTlQLE%X-KA;lj&=zPch=?%sn^=KuOeD!qz?PksP#VTI@Om^{Jg%F=q+&Or%|IJn{ zj}fGM2o5n=p>{d9p;j)G`Bd@#dgEL0)--=D(={EDcPMPLhuCrde4tY@Egi(5N$4XD z`6FxpOsX&&dm7oaOnpE1<h_Co9cQ4CvNOk|uKVK!6d=0f^ zr;HyU#zS0g;p8r~UDW^j{i8v_LU8K|p}T7PmyJFxDLSL@jI2s)k6@b#SaD8Hj9oVN zj*t7RqOaL&HxV~p^1D(gcYFxqKM08OnorH^O=9Z;YbRlXhj@wxZLwc|O*t;;tFOwn zoPlO=<-{0+5*MR!ZU6M6>wbi%9;Ftf3jsIYl@89d;QkzGCL#-@Kg$_Tdo?9lZd^p~ zp6|u#ypU^u{F#$r{&bX~dQPUW*yMsUs-E;EOCo?wo)zNGevEzdu#@V=y@fwtiExkl zbJAHmY@J^7lfe$sU9CR?fKtjhmBf#AVrve1>f0#ONE9R{n``~x+S2tPHs_}?1JC&`eI}%( z0!Y8O-6L!`C?Dg`SO9_*ZZuLeKwc;BaHzGorC+g|VrdCv$}z!=yIm&w5hZ zni;w=cJe-%{&QG2T&o+2XWID)@HmmXm&u6OX0|n>D#$>&-~SKV>|BCt^{}-o5>po= z;>GF7+;0rAxmP)_J|M2NeXBxrXFw?xn+vAu?sh4>=jcR@N9=}Syii3=jJ9bOwfqPW zoSSpQKXg-ZxK7Mrd#d#a@C_4k*kiiFu%cKE)cmSBPe9h@@ZH>5Prhl_W%-&e>v>Q} zztgjMbI0fqIb&Xzl_scC_y8AjCmKPJlLCfgHX0~fTb=aXeQ`53Mh6O=R)*_7+pBog zuy&wlP8sz+0vt9~LP2oGz1HmA?>r+#l08iain+}q-h)%#`tQh-E~`eC(Y33xI`1kE z+V1X3t57{HPqWc48XCxobE^^caH*QG9HeN87=c76&t}3F6FGqcn%iq}Vo#@#>h)wd zLOUUAe{WBFD)!??rBR31*VOxaaqp&AEo$P8suM1!)pKmHt-1JkjaPV}>4$}s=vntW z(912kv#V`Bddqrz=uF^=cb9$On%P&P(NmkA2?MIdSLmgA{$Qq% zySSNq7ywhPr;%G*S0YW(dghTPeftRKjVY*I5!7hk1{>RfV00KDy#iCq|M%_UREgUp z#r3f-PXDPiuyR)UO?LZA{bHL=YVqdIV4DspzD!Q?!oc<7{LcZyOrmleIlu)E;gIuf_P(D2gMT4|mM0K3C_0MW^HE)$_Jn9YU`S7ZT?+fY(t|E8fn?_y>gD zU-yegn{rlZ(u5)zmBdzpCJjZaqikj%8c@v9wQ}PGJ+F;6`vI6uU-6;w5g;MlT~?VC zI{@F@2)|MxU}GJB&{`K4uEtTck>Pc;PC5cunR=G)8CHxciS2suD}0C28_M0rJtB_) zt;dIc^z4}=9;JiGy+)rHLMu}NeL9yt$_|JJ==SrfP7!0 zhYCKnmxxsf_I76QOlzcN+~oqFYJ+DULVXID>TT}0yyPRx8=e!!tmDFRZdiamE8Uzd zzMszgZX^ulbH3-k8_|@xKw`#$+=h0Vw&NlQmtjkoa;eBI;tWGZN#QO;gB8`E*OF|v z6*G@rm%!9bZm0gDLqDjzLy-bVMSALRAwNS%a25Dyt4* z=*a0n+2AM=O|vg@IG@GKt03mk=~l-hGOV@g)*B03`Rjk*L;;r;Z*&Fb(r9QO%@ zc<}B(;9=%p*K}w{fCm;C4z82Ot?zQ_K|-1z{f7aFQ*>xqfEGWf;f%Its z>EFH%W{^~qG7`ZAr=~2%@d2+Af5;k@xs-$~zYaUN(Hu)%m`n01(#_kZwC-gST~JPa zh$M-7+d!3dQo2_qJMsdL+E30R>wUnebAYTS5yBCOMOQs?dW{kQki&KP}0@WUe_ z@^|QK8U^cp(^L@;-8z@{k^PR~5$c3z1$#OKE|h~9mtGz7WEt4fI7vOgo#rG+{{9xx zUxb8peX4JYHn3d+K`NC=M&@*}6#S!%*&;maZ^RF88PcQgry`1V>llQmD`F+} zhAt1Tl+=7(tr4k2w|c~M%>nz0?5qom%I@gEO&f=A&AgsTYwFbtm4`P-sgji7 zdUfl2C1vA-ympD)8Vhj`4=Hgs4`~u3aaL+7A~M!w;%Wsa+Rr3Za7+rQL}>#JKIKtR z;^k3bZ0(2gt&NV*#N`TLxL(-t@+hSEUHSy1zUVS;g7?)2+}B*!+xL0nCGVKsOD(z4 zb=HT<|sHHl<2mscfw&}LO^<drMgCLi^yG+)h+8XLQmI*>B%We8gu`C3>{l1Ptc=@dO-+w9a z53(Rf7UQk3Sn8dqA;8b-9Ipen6KNxEB8yS6p?T>FMS%75bg%5{AOGu)`VP zrAbRB+pvm84qF2`=;*MS-F}dyLbeErCe0j~UeYD)26{fMsT;7Z^&#_GEIe=cxqZOy z?HgXy%6?d&40vYcmFND?)s>#}ySttXmfOo=1!$O0*n&i9H?lK4W^Y@8$M=u`n$Qb% z?nlh0$ZprwrQ9;vRxX(-G(jZm5M9iRWc1iJd_jEIzTby_GjxTR@n(s^?xrr*A~bs6 zTgNwHG8Q$6y+3pXU}7!f7f+kuH$Ad)vw&V%YsWKG;2ql~F%p565qDNtfhS1>svr{4 z6mvW*6oPy1vG?A|i$* zsw2e3Nn2qD>9QmUcZwL?TA_0&*rz(ko_w{fGcy{)VV2<@R}%eoS^InW#cB0$`Fl-I zZ+8A6kokPfSJ{Ji9g?2Hy05U7=Bc+@1oXC;bf`;foK2<2T8tEVLR(kflOiWzQ1d_~ zoxVetVTlQeB_1B05&fVdnZz0+8yN@UQyvim1d{AY^JFS1DNXj)T9p37QvXZgH5e!B z;p*PnIihs{2D9TB736>b+yTi+4epM!YeZYKm@28Da5_ zC8gOecm^TYmf#h`m6B?s4T*ja-(oGGvg~v13-ECyYI9_;DwF1+x!2LzdRVt|^)iMf z_`1F>{ZF5olvPY~1|=&2hgvZX_g}o(GE3QUX>-UWAk%f%ST7C^2L_dN=Ex28!AnU7 zefxcEyA}gO67$hsB-}=Vlla5iE&6yR63ak28blg|D%oeDFo>MP-4Oa#tY1?m*ty%YQ9?pRSkdDc0qGIQQ(`adQRDjZxaSxsKoc;RMvkV|SF_@z_1ieE)bFR~w0U z@}DE@`qQR2hRH{OLZi56L^Ez^QN-RJn&)_SIZROvZgY42jU2c_V|%bAq_EeJt~;F} zes4qJ2q1ere)8WXw&_XkEx{bL;#HG&i|bKKkv2BK<_dd8pbc_9&RGu)Rp8QO6Z6ou z3r{9wi%O2Sb}kTOhe3n5z@#q8$MI{6H@1n*VF#X~hvVB_^0x8g^`T0MDSHkir6a&c zT+(WtuepPCX?zBkU1@-1U_JNMYZ{kT8wa;I6_Q?@fv0laNcY#^9U@M#w68*aaPyU^ z4V*v{J=~$JM(hw+LRK*cOK~g^n+sH!gliR;w=O?B{)Yhu4ZMMKX}3RQ$82-uW`h&6 ze23pPlzUm`Mp;EWaf16i^?Jm>+lCH3@K!Dr{u&wDu(%m-eVzV%&|e^Ma9%aZ>rg@O zuJkT>`hk+Ov23tX+_`xL-Zj15f`vZhje7G&il!r(l$Yd+s|{BtQjIN?(#N)m+eLC_ zCY|{B#T^S!($3-2+?YAHlp}yAr87~QspJxg$RF7crNq%xrDMM%V904pgY&HTaBll-68Y!&)ha1$+(b(8# z_8xm!GEk+2(s-c4Adt1VOPTCyH|9`mvclT+>`u%ylhF`zm_qr!9e42?K>gXnBw!jb zy!1vs7B4-h*V^+j?`$$xz-&b4wn*F0Fu~JNGSKNuM|Je^qp+>!;Kr zbOJ;z@$a$<%2*}PYr6YrF;!$BQ7D^XShFF zRE&)8_dqrQ>w{rRO0nXeT&i=8*k|z}vGF-%wDO%mxf;a=;H-#KA_%&JP$V=>G504YD2#Zi4;L}`RtLRH#OdsrVteXzvK<@(DBe@_u3)^nToY?M$v78_K*rU2jCjI;(R~8%A>xo-2IR@&1yBS7}S;;|>1 zE7ve#tz2v8*?Zu31Q;vfIG^9n%_>G}uq}^IVSG1S)U3F(gTG?XvJV#1;C)u)a=`tF z7ZBY&0=TSjjsQNtHTW8OBQnr>!I0x+v8n5*RlIOWB^&}p_D3Gq<)9zgVE#}wt>S@g zcez*3Ac0Vyrl+8LNSj_a4&T~G_EM?Sn=5O(Kx(M>(8xkPvX`&$2;g8-i+2J#aPN_k z3pp-FfT(c`#dd|E=z?0(W%>>*{p+y8PM6*f4}Z! - + + @@ -13,4 +13,4 @@ - + \ No newline at end of file diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop index 9076493bb8..40b241e05d 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Type=Application Name=AppFlowy -Icon=io.appflowy.AppFlowy -Exec=AppFlowy %U +Icon=io.appflowy.appflowy +Exec=appflowy %U Categories=Network;Productivity; Keywords=Notes DBusActivatable=true diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml index c9a58b68fa..ea3b476004 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml @@ -1,38 +1,46 @@ - io.appflowy.AppFlowy - + io.appflowy.appflowy + AppFlowy - Open Source Notion Alternative - + Open Source Notion Alternative + CC-BY-4.0 AGPL-3.0-only - +

- # Built for teams that need more control and flexibility ## 100% data control You can host AppFlowy wherever you want; no vendor lock-in. + # Built for teams that need more control and flexibility ## 100% data control You can host + AppFlowy wherever you want; no vendor lock-in.

## Unlimited customizations Design and modify AppFlowy your way with an open core codebase.

- ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance. + ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What + does this mean? Faster development, better native experience, and more reliable performance.

- # Built for individuals who care about data security and mobile experience ## 100% control of your data Download and install AppFlowy on your local machine. You own and control your personal data. + # Built for individuals who care about data security and mobile experience ## 100% control of + your data Download and install AppFlowy on your local machine. You own and control your + personal data.

- ## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It's built on a community-driven toolbox, including templates, plugins, themes, and more. + ## Extensively extensible For those with no coding experience, AppFlowy enables you to create + apps that suit your needs. It's built on a community-driven toolbox, including templates, + plugins, themes, and more.

- ## Truly native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web. + ## Truly native experience Faster, more stable with support for offline mode. It's also + better integrated with different devices. Moreover, AppFlowy enables users to access features + and possibilities not available on the web.

- - io.appflowy.AppFlowy.desktop + + io.appflowy.appflowy.desktop https://github.com/AppFlowy-IO/appflowy/raw/main/doc/imgs/welcome.png -
+ \ No newline at end of file diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service index 31e32415d0..fed8eabcf1 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service @@ -1,3 +1,3 @@ [D-BUS Service] -Name=io.appflowy.AppFlowy -Exec=AppFlowy +Name=io.appflowy.appflowy +Exec=appflowy diff --git a/frontend/scripts/flatpack-buildfiles/launcher.sh b/frontend/scripts/flatpack-buildfiles/launcher.sh index 24b4fdbea4..5cda51d0a9 100644 --- a/frontend/scripts/flatpack-buildfiles/launcher.sh +++ b/frontend/scripts/flatpack-buildfiles/launcher.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -gdbus call --session --dest io.appflowy.AppFlowy \ +gdbus call --session --dest io.appflowy.appflowy \ --object-path /io/appflowy/AppFlowy/Object \ - --method io.appflowy.AppFlowy.Open "['$1']" {} + --method io.appflowy.appflowy.Open "['$1']" {} diff --git a/frontend/scripts/flutter_release_build/build_linux.sh b/frontend/scripts/flutter_release_build/build_linux.sh new file mode 100755 index 0000000000..21f591193a --- /dev/null +++ b/frontend/scripts/flutter_release_build/build_linux.sh @@ -0,0 +1,243 @@ +#!/bin/bash +# This Script is used to build the AppFlowy linux zip, deb, rpm or appimage +# +# Usage: ./scripts/flutter_release_build/build_linux.sh --build_type --build_arch --version [--skip-code-generation] [--skip-rebuild-core] +# +# Options: +# -h, --help Show this help message and exit +# --build_type The type of package to build. Must be one of: +# - all: Build all package types +# - zip: Build only zip package +# - tar.xz: Build only tar.xz package +# - deb: Build only deb package +# - rpm: Build only rpm package +# - appimage: Build only appimage package +# --build_arch The architecture to build. Must be one of: +# - x86_64: Build for x86_64 architecture +# - arm64: Build for arm64 architecture (not supported yet) +# --version The version number (e.g. 0.8.2) +# --skip-code-generation Skip the code generation step +# --skip-rebuild-core Skip the core rebuild step + +show_help() { + echo "Usage: ./scripts/flutter_release_build/build_linux.sh --build_type --build_arch --version [--skip-code-generation] [--skip-rebuild-core]" + echo "" + echo "Options:" + echo " -h, --help Show this help message and exit" + echo "" + echo "Arguments:" + echo " --build_type The type of package to build. Must be one of:" + echo " - all: Build all package types" + echo " - zip: Build only zip package" + echo " - tar.xz: Build only tar.xz package" + echo " - deb: Build only deb package" + echo " - rpm: Build only rpm package" + echo " Please install the \033[33mrpm-build\033[0m and \033[33mpatchelf\033[0m before building the rpm and appimage package." + echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/rpm/." + echo " - appimage: Build only appimage package" + echo " Please install the \033[33mlocate\033[0m and \033[33mappimagetool\033[0m before building the appimage package." + echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/appimage/." + echo " --build_arch The architecture to build. Must be one of:" + echo " - x86_64: Build for x86_64 architecture" + echo " - arm64: Build for arm64 architecture (not supported yet)" + echo " --version The version number (e.g. 0.8.2)" + echo " --skip-code-generation Skip the code generation step. It may save time if you have already generated the code." + echo " --skip-rebuild-core Skip the core rebuild step. It may save time if you have already built the core." + exit 0 +} + +# Check for help flag +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + show_help +fi + +# Parse named arguments +while [ $# -gt 0 ]; do + case "$1" in + --build_type) + BUILD_TYPE="$2" + shift 2 + ;; + --build_arch) + BUILD_ARCH="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --skip-code-generation) + SKIP_CODE_GENERATION=true + shift + ;; + --skip-rebuild-core) + SKIP_REBUILD_CORE=true + shift + ;; + *) + echo "Unknown parameter: $1" + show_help + exit 1 + ;; + esac +done + +clear_cache() { + echo -e "Clearing the cache..." + rm -rf appflowy_flutter/build/$VERSION/ +} + +info() { + echo -e "🚀 \033[32m$1\033[0m" +} + +error() { + echo -e "🚨 \033[31m$1\033[0m" +} + +# Validate build type argument +if [ -z "$BUILD_TYPE" ]; then + error "Please specify build type with --build_type: all, zip, tar.xz, deb, rpm, appimage" + exit 1 +fi + +# Validate version argument +if [ -z "$VERSION" ]; then + error "Please specify version number with --version (e.g. 0.8.2)" + exit 1 +fi + +# Validate build arch argument +if [ -z "$BUILD_ARCH" ]; then + error "Please specify build arch with --build_arch: x86_64, arm64 or universal" + exit 1 +fi + +if [ "$BUILD_TYPE" != "all" ] && [ "$BUILD_TYPE" != "zip" ] && [ "$BUILD_TYPE" != "tar.xz" ] && [ "$BUILD_TYPE" != "deb" ] && [ "$BUILD_TYPE" != "rpm" ] && [ "$BUILD_TYPE" != "appimage" ]; then + error "Invalid build type. Must be one of: all, zip, tar.xz, deb, rpm, appimage" + exit 1 +fi + +has_built_core=false +has_generated_code=false + +prepare_build() { + info "Preparing build..." + + # Build the rust-lib with version + if [ "$SKIP_REBUILD_CORE" != "true" ] && [ "$has_built_core" != "true" ]; then + cargo make --env APP_VERSION=$VERSION --profile production-linux-$BUILD_ARCH appflowy-core-release + has_built_core=true + fi + + if [ "$SKIP_CODE_GENERATION" != "true" ] && [ "$has_generated_code" != "true" ]; then + cargo make --env APP_VERSION=$VERSION --profile production-linux-$BUILD_ARCH code_generation + has_generated_code=true + fi +} + +build_zip() { + info "Building zip package version $VERSION..." + + prepare_build + + cd appflowy_flutter + flutter_distributor release --name=prod --jobs=release-prod-linux-zip --skip-clean + cd .. + mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.zip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip + + info "Zip package built successfully. The zip package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip" +} + +build_deb() { + info "Building deb package version $VERSION..." + + prepare_build + + cd appflowy_flutter + flutter_distributor release --name=prod --jobs=release-prod-linux-deb --skip-clean + cd .. + mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.deb appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.deb + + info "Deb package built successfully. The deb package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.deb" +} + +build_rpm() { + info "Building rpm package version $VERSION..." + + prepare_build + + cd appflowy_flutter + flutter_distributor release --name=prod --jobs=release-prod-linux-rpm --skip-clean + cd .. + mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.rpm appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.rpm + + info "RPM package built successfully. The RPM package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.rpm" +} + +# Function to build AppImage package +build_appimage() { + info "Building AppImage package version $VERSION..." + + prepare_build + + cd appflowy_flutter + flutter_distributor release --name=prod --jobs=release-prod-linux-appimage --skip-clean + cd .. + mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.AppImage appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.AppImage + + info "AppImage package built successfully. The AppImage package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.AppImage" +} + +build_tar_xz() { + info "Building tar.xz package version $VERSION..." + + prepare_build + + # step 1: check if the linux zip package is built, if not, build the zip package + if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip" ]; then + info "Linux zip package is not built. Building the zip package..." + build_zip + fi + + # step 2: unzip the zip package + unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip -d appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64 + + # check if the AppFlowy directory exists + if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64" ]; then + error "AppFlowy directory doesn't exist. Please check the zip package." + exit 1 + fi + + # step 3: build the tar.xz package + tar -cJvf appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.tar.xz appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64/* + + info "Tar.xz package built successfully. The tar.xz package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.tar.xz" +} + +clear_cache + +# Build packages based on build type +case $BUILD_TYPE in +"all") + build_zip + build_deb + build_rpm + build_appimage + ;; +"zip") + build_zip + ;; +"deb") + build_deb + ;; +"rpm") + build_rpm + ;; +"appimage") + build_appimage + ;; +"tar.xz") + build_tar_xz + ;; +esac diff --git a/frontend/scripts/flutter_release_build/build_macos.sh b/frontend/scripts/flutter_release_build/build_macos.sh new file mode 100755 index 0000000000..82071bef38 --- /dev/null +++ b/frontend/scripts/flutter_release_build/build_macos.sh @@ -0,0 +1,297 @@ +# This Script is used to build the AppFlowy macOS zip, dmg or pkg +# +# Usage: ./scripts/flutter_release_build/build_macos.sh --build_type --build_arch --version --apple-id --team-id --password [--skip-code-generation] [--skip-rebuild-core] +# +# Options: +# -h, --help Show this help message and exit +# --build_type The type of package to build. Must be one of: +# - all: Build all package types +# - zip: Build only zip package +# - dmg: Build only dmg package +# - tar.xz: Build only tar.xz package +# --build_arch The architecture to build. Must be one of: +# - x86_64: Build for x86_64 architecture +# - arm64: Build for arm64 architecture +# - universal: Build for universal architecture +# --version The version number (e.g. 0.8.2) +# --skip-code-generation Skip the code generation step +# --skip-rebuild-core Skip the core rebuild step +# --apple-id The apple id to use for the notary service +# --team-id The team id to use for the notary service +# --password The password to use for the notary service + +show_help() { + echo "Usage: ./scripts/flutter_release_build/build_macos.sh --build_type --build_arch --version --apple-id --team-id --password [--skip-code-generation] [--skip-rebuild-core]" + echo "" + echo "Options:" + echo " -h, --help Show this help message and exit" + echo "" + echo "Arguments:" + echo " --build_type The type of package to build. Must be one of:" + echo " - all: Build all package types" + echo " - zip: Build only zip package" + echo " Please install the \033[33mp7zip\033[0m before building the zip package." + echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/zip/." + echo " - tar.xz: Build only tar.xz package" + echo " - dmg: Build only dmg package" + echo " Please install the \033[33mappdmg\033[0m before building the dmg package." + echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/dmg/." + echo " --build_arch The architecture to build. Must be one of:" + echo " - x86_64: Build for x86_64 architecture" + echo " - arm64: Build for arm64 architecture" + echo " - universal: Build for universal architecture" + echo " --version The version number (e.g. 0.8.2)" + echo " --skip-code-generation Skip the code generation step. It may save time if you have already generated the code." + echo " --skip-rebuild-core Skip the core rebuild step. It may save time if you have already built the core." + echo " --apple-id The apple id to use for the notary service" + echo " --team-id The team id to use for the notary service" + echo " --password The password to use for the notary service" + exit 0 +} + +# Check for help flag +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + show_help +fi + +# Parse named arguments +while [ $# -gt 0 ]; do + case "$1" in + --build_type) + BUILD_TYPE="$2" + shift 2 + ;; + --build_arch) + BUILD_ARCH="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --skip-code-generation) + SKIP_CODE_GENERATION=true + shift + ;; + --skip-rebuild-core) + SKIP_REBUILD_CORE=true + shift + ;; + --apple-id) + APPLE_ID="$2" + shift 2 + ;; + --team-id) + TEAM_ID="$2" + shift 2 + ;; + --password) + PASSWORD="$2" + shift 2 + ;; + *) + echo "Unknown parameter: $1" + show_help + exit 1 + ;; + esac +done + +clear_cache() { + echo "Clearing the cache..." + rm -rf appflowy_flutter/build/$VERSION/ +} + +info() { + echo "🚀 \033[32m$1\033[0m" +} + +error() { + echo "🚨 \033[31m$1\033[0m" +} + +# Validate build type argument +if [ -z "$BUILD_TYPE" ]; then + error "Please specify build type with --build_type: all, zip, dmg, tar.xz" + exit 1 +fi + +# Validate version argument +if [ -z "$VERSION" ]; then + error "Please specify version number with --version (e.g. 0.8.2)" + exit 1 +fi + +# Validate build arch argument +if [ -z "$BUILD_ARCH" ]; then + error "Please specify build arch with --build_arch: x86_64, arm64 or universal" + exit 1 +fi + +if [ "$BUILD_TYPE" != "all" ] && [ "$BUILD_TYPE" != "zip" ] && [ "$BUILD_TYPE" != "dmg" ] && [ "$BUILD_TYPE" != "tar.xz" ]; then + error "Invalid build type. Must be one of: all, zip, dmg, tar.xz" + exit 1 +fi + +prepare_build() { + info "Preparing build..." + + # step 1: build the appflowy-core (rust-lib) based on the build arch + if [ "$SKIP_REBUILD_CORE" != "true" ]; then + if [ "$BUILD_ARCH" = "x86_64" ] || [ "$BUILD_ARCH" = "universal" ]; then + info "Building appflowy-core for x86_64...(This may take a while)" + cargo make --profile production-mac-x86_64 appflowy-core-release + fi + + if [ "$BUILD_ARCH" = "arm64" ] || [ "$BUILD_ARCH" = "universal" ]; then + info "Building appflowy-core for arm64...(This may take a while)" + cargo make --profile production-mac-arm64 appflowy-core-release + fi + + # step 2 (optional): combine these two libdart_ffi.a into one libdart_ffi.a if the build arch is universal + if [ "$BUILD_ARCH" = "universal" ]; then + info "Combining libdart_ffi.a for universal..." + lipo -create \ + rust-lib/target/x86_64-apple-darwin/release/libdart_ffi.a \ + rust-lib/target/aarch64-apple-darwin/release/libdart_ffi.a \ + -output rust-lib/target/libdart_ffi.a + + info "Checking the libdart_ffi.a for universal..." + lipo -archs rust-lib/target/libdart_ffi.a + + cp -rf rust-lib/target/libdart_ffi.a \ + appflowy_flutter/packages/appflowy_backend/macos/ + fi + fi + + # step 3 (optional): generate the flutter code: languages, icons and freezed files. + if [ "$SKIP_CODE_GENERATION" != "true" ]; then + info "Generating the flutter code...(This may take a while)" + cargo make code_generation + fi + + # step 4: build the zip package + info "Building the zip package..." + cd appflowy_flutter + flutter_distributor release --name=prod --jobs=release-prod-macos-zip --skip-clean + cd .. +} + +build_zip() { + info "Building zip package version $VERSION..." + + # step 1: check if the macos zip package is built, if not, build the zip package + if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then + info "macOS zip package is not built. Building the zip package..." + prepare_build + + # step 1.1: move the zip package to the build directory + mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-macos.zip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip + fi + + # step 2: unzip the zip package and codesign the app + unzip -o appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip + + # step 3: codesign the app + # note: You must install the certificate to the system before codesigning + sudo /usr/bin/codesign --force --options runtime --deep --sign "Developer ID Application: APPFLOWY PTE. LTD" --deep --verbose AppFlowy.app -v + + # step 4: zip the app again + 7z a appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip AppFlowy.app + + info "Zip package built successfully" +} + +build_dmg() { + info "Building DMG package version $VERSION..." + + # step 1: check if the macos zip package is built, if not, build the zip package + if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then + info "macOS zip package is not built. Building the zip package..." + build_zip + fi + + # step 2: unzip the zip package and copy the make_config.json file to the build directory + unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip -d appflowy_flutter/build/$VERSION/ + cp appflowy_flutter/macos/packaging/dmg/make_config.json appflowy_flutter/build/$VERSION/ + + # check if the AppFlowy.app doesn't exist, exit the script + if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy.app" ]; then + error "AppFlowy.app doesn't exist. Please check the zip package." + exit 1 + fi + + # check if the appdmg has been installed + if ! command -v appdmg &>/dev/null; then + info "appdmg is not installed. Installing appdmg..." + npm install -g appdmg + fi + + # step 3: build the dmg package using appdmg + # note: You must install the appdmg to the system before building the dmg package + appdmg appflowy_flutter/build/$VERSION/make_config.json appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg + + # step 4: clear the temp files + rm -rf appflowy_flutter/build/$VERSION/AppFlowy.app + rm -rf appflowy_flutter/build/$VERSION/make_config.json + + # check if the dmg package is built + if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg" ]; then + error "DMG package is not built. Please check the build process." + exit 1 + fi + + info "DMG package built successfully. The dmg package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg" + + if [ -z "$APPLE_ID" ] || [ -z "$TEAM_ID" ] || [ -z "$PASSWORD" ]; then + error "The apple id, team id and password are not specified. Please notarize the dmg package manually." + error "xcrun notarytool submit appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg --apple-id --team-id --password -v -f \"json\" --wait" + else + xcrun notarytool submit appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg --apple-id $APPLE_ID --team-id $TEAM_ID --password $PASSWORD -v -f "json" --wait + info "Notarization is completed. Please check the notarization status" + fi +} + +build_tar_xz() { + info "Building tar.xz package version $VERSION..." + + # step 1: check if the macos zip package is built, if not, build the zip package + if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then + info "macOS zip package is not built. Building the zip package..." + build_zip + fi + + # step 2: unzip the zip package and copy the make_config.json file to the build directory + unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip -d appflowy_flutter/build/$VERSION/ + + # check if the AppFlowy.app doesn't exist, exit the script + if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy.app" ]; then + error "AppFlowy.app doesn't exist. Please check the zip package." + exit 1 + fi + + # step 3: build the tar.xz package + tar -cJvf appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.tar.xz appflowy_flutter/build/$VERSION/AppFlowy.app + + info "Tar.xz package built successfully. The tar.xz package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.tar.xz" +} + +clear_cache + +# Build packages based on build type +case $BUILD_TYPE in +"all") + build_zip + build_dmg + build_tar_xz + ;; +"zip") + build_zip + ;; +"dmg") + build_dmg + ;; +"tar.xz") + build_tar_xz + ;; +esac diff --git a/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml index cd8103df9e..45d52d6044 100644 --- a/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml +++ b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml @@ -11,11 +11,11 @@ script: AppDir: path: ./AppDir app_info: - id: io.appflowy.AppFlowy + id: io.appflowy.appflowy name: AppFlowy icon: appflowy.svg version: [CHANGE_THIS] - exec: AppFlowy + exec: appflowy exec_args: $@ apt: arch: diff --git a/frontend/scripts/linux_distribution/deb/AppFlowy.desktop b/frontend/scripts/linux_distribution/deb/AppFlowy.desktop index e6851f9f42..6b485fc50e 100644 --- a/frontend/scripts/linux_distribution/deb/AppFlowy.desktop +++ b/frontend/scripts/linux_distribution/deb/AppFlowy.desktop @@ -2,7 +2,7 @@ Type=Application Name=AppFlowy Icon=/usr/share/icons/hicolor/scalable/apps/appflowy.svg -Exec=/usr/bin/AppFlowy %U +Exec=/usr/bin/appflowy %U Categories=Network;Productivity; Keywords=Notes Terminal=false diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst index bf2f79fa97..fbc5b1fc91 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst @@ -1,8 +1,8 @@ #!/usr/bin/env bash -if [ -e /usr/bin/AppFlowy ]; then +if [ -e /usr/bin/appflowy ]; then echo "Symlink already exists, skipping." else echo "Creating Symlink in /usr/bin/appflowy" - ln -s /usr/lib/AppFlowy/AppFlowy /usr/bin/AppFlowy + ln -s /usr/lib/appflowy/appflowy /usr/bin/appflowy ln -s /usr/lib/AppFlowy/launcher.sh /usr/bin/AppFlowyLauncher.sh fi diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm index 59a680e767..f0131be6f6 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm @@ -1,5 +1,5 @@ #!/usr/bin/env bash -if [ -e /usr/bin/AppFlowy ]; then - rm /usr/bin/AppFlowy +if [ -e /usr/bin/appflowy ]; then + rm /usr/bin/appflowy rm /usr/bin/AppFlowyLauncher.sh fi diff --git a/frontend/scripts/linux_distribution/deb/build_deb.sh b/frontend/scripts/linux_distribution/deb/build_deb.sh index 42fbf7346d..a5fb5bc53a 100644 --- a/frontend/scripts/linux_distribution/deb/build_deb.sh +++ b/frontend/scripts/linux_distribution/deb/build_deb.sh @@ -25,9 +25,9 @@ chmod 0755 $DEBIAN/postinst chmod 0755 $DEBIAN/postrm grep -rl "\[CHANGE_THIS\]" $DEBIAN/control | xargs sed -i "s/\[CHANGE_THIS\]/$VERSION/" -cp -fR $LINUX_RELEASE_PRODUCTION/AppFlowy $LIB -cp ./scripts/linux_distribution/deb/AppFlowy.desktop $APPLICATIONS -cp ./scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml $METAINFO +cp -fR $LINUX_RELEASE_PRODUCTION/appflowy $LIB +cp ./scripts/linux_distribution/deb/appflowy.desktop $APPLICATIONS +cp ./scripts/linux_distribution/packaging/io.appflowy.appflowy.metainfo.xml $METAINFO cp ./scripts/linux_distribution/packaging/appflowy.svg $ICONS # Build the package diff --git a/frontend/scripts/linux_distribution/flatpak/README.md b/frontend/scripts/linux_distribution/flatpak/README.md index 7d90cfd2a6..b1d40056da 100644 --- a/frontend/scripts/linux_distribution/flatpak/README.md +++ b/frontend/scripts/linux_distribution/flatpak/README.md @@ -1 +1 @@ -Please refer to https://github.com/flathub/io.appflowy.AppFlowy repo. +Please refer to https://github.com/flathub/io.appflowy.appflowy repo. diff --git a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml index c9a58b68fa..ea3b476004 100644 --- a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml +++ b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml @@ -1,38 +1,46 @@ - io.appflowy.AppFlowy - + io.appflowy.appflowy + AppFlowy - Open Source Notion Alternative - + Open Source Notion Alternative + CC-BY-4.0 AGPL-3.0-only - +

- # Built for teams that need more control and flexibility ## 100% data control You can host AppFlowy wherever you want; no vendor lock-in. + # Built for teams that need more control and flexibility ## 100% data control You can host + AppFlowy wherever you want; no vendor lock-in.

## Unlimited customizations Design and modify AppFlowy your way with an open core codebase.

- ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance. + ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What + does this mean? Faster development, better native experience, and more reliable performance.

- # Built for individuals who care about data security and mobile experience ## 100% control of your data Download and install AppFlowy on your local machine. You own and control your personal data. + # Built for individuals who care about data security and mobile experience ## 100% control of + your data Download and install AppFlowy on your local machine. You own and control your + personal data.

- ## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It's built on a community-driven toolbox, including templates, plugins, themes, and more. + ## Extensively extensible For those with no coding experience, AppFlowy enables you to create + apps that suit your needs. It's built on a community-driven toolbox, including templates, + plugins, themes, and more.

- ## Truly native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web. + ## Truly native experience Faster, more stable with support for offline mode. It's also + better integrated with different devices. Moreover, AppFlowy enables users to access features + and possibilities not available on the web.

- - io.appflowy.AppFlowy.desktop + + io.appflowy.appflowy.desktop https://github.com/AppFlowy-IO/appflowy/raw/main/doc/imgs/welcome.png -
+ \ No newline at end of file diff --git a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service index 31e32415d0..2982dff894 100644 --- a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service +++ b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service @@ -1,3 +1,3 @@ [D-BUS Service] -Name=io.appflowy.AppFlowy +Name=io.appflowy.appflowy Exec=AppFlowy diff --git a/frontend/scripts/linux_distribution/packaging/launcher.sh b/frontend/scripts/linux_distribution/packaging/launcher.sh index 24b4fdbea4..263eca6593 100644 --- a/frontend/scripts/linux_distribution/packaging/launcher.sh +++ b/frontend/scripts/linux_distribution/packaging/launcher.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -gdbus call --session --dest io.appflowy.AppFlowy \ - --object-path /io/appflowy/AppFlowy/Object \ - --method io.appflowy.AppFlowy.Open "['$1']" {} +gdbus call --session --dest io.appflowy.appflowy \ + --object-path /io/appflowy/appflowy/Object \ + --method io.appflowy.appflowy.Open "['$1']" {} diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst index 83e1a1043e..82a03114c0 100644 --- a/frontend/scripts/linux_installer/postinst +++ b/frontend/scripts/linux_installer/postinst @@ -1,7 +1,7 @@ #!/usr/bin/env bash -if [ -e /usr/local/bin/AppFlowy ]; then +if [ -e /usr/local/bin/appflowy ]; then echo "Symlink already exists, skipping." else echo "Creating Symlink in /usr/local/bin/appflowy" - ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy + ln -s /opt/appflowy/appflowy /usr/local/bin/appflowy fi From e9a1a1ced02bdaa7b44afbe20570a195004315b4 Mon Sep 17 00:00:00 2001 From: Annie Date: Sun, 23 Feb 2025 13:46:13 +0800 Subject: [PATCH 045/384] chore: Update README.md (#7411) change appflowy.io to appflowy.com wherever possible --- README.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b9606b8844..565908e756 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- AppFlowy.IO
+ AppFlowy
⭐️ The Open Source Alternative To Notion ⭐️

@@ -18,18 +18,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo

- Website • + WebsiteForumDiscordRedditTwitter

-

AppFlowy Kanban Board for To-dos

-

AppFlowy Databases for Tasks and Projects

-

AppFlowy Sites for Beautiful documentation

-

AppFlowy AI

-

AppFlowy Templates

+

AppFlowy Kanban Board for To-dos

+

AppFlowy Databases for Tasks and Projects

+

AppFlowy Sites for Beautiful documentation

+

AppFlowy AI

+

AppFlowy Templates



@@ -48,7 +48,7 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo - [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone - [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported -- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy) +- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview) - [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source) ## Built With @@ -78,7 +78,7 @@ report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labe ## **Releases** -Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release. +Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release. ## Contributing @@ -89,9 +89,7 @@ for details. If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains -the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with -us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! -Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. +the community, **Congratulations!** You are now an official contributor to AppFlowy. ## Translations 🌎🗺 @@ -152,8 +150,8 @@ more information. ## Acknowledgments -Special thanks to these amazing projects which help power AppFlowy.IO: +Special thanks to these amazing projects which help power AppFlowy: - [cargo-make](https://github.com/sagiegurari/cargo-make) - [contrib.rocks](https://contrib.rocks) -- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui) \ No newline at end of file +- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui) From 63239893abf1988626fca7e189a3b8f277d49698 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:14:26 +0800 Subject: [PATCH 046/384] chore(flutter): move other user message to end (#7413) --- .../lib/plugins/ai_chat/chat_page.dart | 2 -- .../message/user_message_bubble.dart | 27 +++++-------------- .../message/user_text_message.dart | 3 --- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 650840e019..ee39fb8adc 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -195,7 +195,6 @@ class _ChatContentPage extends StatelessWidget { return ChatUserMessageWidget( user: message.author, message: message, - isCurrentUser: true, ); } @@ -203,7 +202,6 @@ class _ChatContentPage extends StatelessWidget { return ChatUserMessageWidget( user: message.author, message: message, - isCurrentUser: false, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart index e70f346ecc..81aab4d555 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart @@ -15,13 +15,11 @@ class ChatUserMessageBubble extends StatelessWidget { super.key, required this.message, required this.child, - required this.isCurrentUser, this.files = const [], }); final Message message; final Widget child; - final bool isCurrentUser; final List files; @override @@ -46,31 +44,18 @@ class ChatUserMessageBubble extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: getChildren(context), + children: [ + const Spacer(), + _buildBubble(context), + const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), + _buildAvatar(), + ], ), ], ), ); } - List getChildren(BuildContext context) { - if (isCurrentUser) { - return [ - const Spacer(), - _buildBubble(context), - const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), - _buildAvatar(), - ]; - } else { - return [ - _buildAvatar(), - const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), - _buildBubble(context), - const Spacer(), - ]; - } - } - Widget _buildAvatar() { return BlocBuilder( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index 3638ace12f..c73100b59d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -15,12 +15,10 @@ class ChatUserMessageWidget extends StatelessWidget { super.key, required this.user, required this.message, - required this.isCurrentUser, }); final User user; final TextMessage message; - final bool isCurrentUser; @override Widget build(BuildContext context) { @@ -34,7 +32,6 @@ class ChatUserMessageWidget extends StatelessWidget { ), child: ChatUserMessageBubble( message: message, - isCurrentUser: isCurrentUser, files: _getFiles(), child: BlocBuilder( builder: (context, state) { From 7eaafc52ce281ee6b8a932c18a47d9d20cdbc79f Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:35:53 +0800 Subject: [PATCH 047/384] fix: adjust other user message alignment (#7414) --- .../lib/plugins/ai_chat/chat_page.dart | 10 ++-------- .../presentation/message/user_message_bubble.dart | 3 +-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index ee39fb8adc..551a210d00 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -191,14 +191,8 @@ class _ChatContentPage extends StatelessWidget { ); } - if (message.author.id == userProfile.id.toString()) { - return ChatUserMessageWidget( - user: message.author, - message: message, - ); - } - - if (isOtherUserMessage(message)) { + if (message.author.id == userProfile.id.toString() || + isOtherUserMessage(message)) { return ChatUserMessageWidget( user: message.author, message: message, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart index 81aab4d555..8f897fc8a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart @@ -42,10 +42,9 @@ class ChatUserMessageBubble extends StatelessWidget { const VSpace(6), ], Row( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, children: [ - const Spacer(), _buildBubble(context), const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), _buildAvatar(), From c5fa9039b4f7b2715642ec4d592367f8ade2263b Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 26 Feb 2025 10:06:28 +0800 Subject: [PATCH 048/384] fix: add shortcut to create Inline Math Equation (#7401) * fix: add shortcut to create Math Equation(#7331) * chore: update code Co-authored-by: Lucas --------- Co-authored-by: Lucas --- .../document/document_alignment_test.dart | 2 +- ...cument_with_inline_math_equation_test.dart | 51 +++++++++++++ .../custom_text_align_command.dart | 9 +-- .../math_equation/math_equation_shortcut.dart | 74 +++++++++++++++++++ .../shortcuts/command_shortcuts.dart | 2 + .../document/presentation/editor_style.dart | 4 + .../pages/settings_shortcuts_view.dart | 5 ++ frontend/resources/translations/en.json | 1 + 8 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index d95d907881..ea8db6fdad 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -75,7 +75,7 @@ void main() { [ LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyC, ], tester: tester, withKeyUp: true, diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index 45f688bd58..906b5ab69c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -4,6 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -163,5 +164,55 @@ void main() { lessThan(5), ); }); + + testWidgets('insert inline math equation by shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + // create a new document + await tester.createNewPageWithNameUnderParent( + name: 'insert inline math equation by shortcut', + ); + + // tap the first line of the document + await tester.editor.tapLineOfEditorAt(0); + // insert a inline page + const formula = 'E = MC ^ 2'; + await tester.ime.insertText(formula); + await tester.editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: formula.length), + ); + + // mock key event + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyE, + isShiftPressed: true, + isControlPressed: true, + ); + + // expect to see the math equation block + final inlineMathEquation = find.byType(InlineMathEquation); + expect(inlineMathEquation, findsOneWidget); + + await tester.editor.tapLineOfEditorAt(0); + const text = 'Hello World'; + await tester.ime.insertText(text); + + final inlineText = find.textContaining(text, findRichText: true); + expect(inlineText, findsOneWidget); + + // the text should be in the same line with the math equation + final inlineMathEquationPosition = tester.getRect(inlineMathEquation); + final textPosition = tester.getRect(inlineText); + // allow 5px difference + expect( + (textPosition.top - inlineMathEquationPosition.top).abs(), + lessThan(5), + ); + expect( + (textPosition.bottom - inlineMathEquationPosition.bottom).abs(), + lessThan(5), + ); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart index 9fe78591c3..bc3f5cffa1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; final List customTextAlignCommands = [ customTextLeftAlignCommand, @@ -26,8 +25,8 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey), ); -/// Windows / Linux : ctrl + shift + e -/// macOS : ctrl + shift + e +/// Windows / Linux : ctrl + shift + c +/// macOS : ctrl + shift + c /// Allows the user to align text to the center /// /// - support @@ -36,7 +35,7 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( /// final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( key: 'Align text to the center', - command: 'ctrl+shift+e', + command: 'ctrl+shift+c', getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr, handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart new file mode 100644 index 0000000000..9c9fe7905b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart @@ -0,0 +1,74 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +/// Windows / Linux : ctrl + shift + e +/// macOS : cmd + shift + e +/// Allows the user to insert math equation by shortcut +/// +/// - support +/// - desktop +/// - web +/// +final CommandShortcutEvent insertInlineMathEquationCommand = + CommandShortcutEvent( + key: 'Insert inline math equation', + command: 'ctrl+shift+e', + macOSCommand: 'cmd+shift+e', + getDescription: LocaleKeys.document_plugins_mathEquation_name.tr, + handler: (editorState) { + final selection = editorState.selection; + if (selection == null || selection.isCollapsed || !selection.isSingle) { + return KeyEventResult.ignored; + } + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return KeyEventResult.ignored; + } + if (node.delta == null || !toolbarItemWhiteList.contains(node.type)) { + return KeyEventResult.ignored; + } + final transaction = editorState.transaction; + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[InlineMathEquationKeys.formula] != null, + ); + }); + if (isHighlight) { + final formula = delta + .slice(selection.startIndex, selection.endIndex) + .whereType() + .firstOrNull + ?.attributes?[InlineMathEquationKeys.formula]; + assert(formula != null); + if (formula == null) { + return KeyEventResult.ignored; + } + // clear the format + transaction.replaceText( + node, + selection.startIndex, + selection.length, + formula, + attributes: {}, + ); + } else { + final text = editorState.getTextInSelection(selection).join(); + transaction.replaceText( + node, + selection.startIndex, + selection.length, + MentionBlockKeys.mentionChar, + attributes: { + InlineMathEquationKeys.formula: text, + }, + ); + } + editorState.apply(transaction); + return KeyEventResult.handled; + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart index 1f409255c6..aedfcff432 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart'; @@ -39,6 +40,7 @@ List commandShortcutEvents = [ ...customTextAlignCommands, customDeleteCommand, + insertInlineMathEquationCommand, // remove standard shortcuts for copy, cut, paste, todo ...standardCommandShortcutEvents diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 716296e51f..c5df72dd0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -494,6 +494,10 @@ class EditorStyleCustomizer { 'italic': (LocaleKeys.toolbar_italic.tr(), 'I'), 'strikethrough': (LocaleKeys.toolbar_strike.tr(), 'Shift+S'), 'code': (LocaleKeys.toolbar_inlineCode.tr(), 'E'), + 'editor.inline_math_equation': ( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), + 'Shift+E' + ), }; final markdownItemIds = markdownItemTooltips.keys.toSet(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart index 1db39b8fda..3552205ba4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; @@ -597,6 +598,10 @@ extension CommandLabel on CommandShortcutEvent { label = LocaleKeys.settings_shortcutsPage_keybindings_alignCenter.tr(); } else if (key == customTextRightAlignCommand.key) { label = LocaleKeys.settings_shortcutsPage_keybindings_alignRight.tr(); + } else if (key == insertInlineMathEquationCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_insertInlineMathEquation + .tr(); } else if (key == undoCommand.key) { label = LocaleKeys.settings_shortcutsPage_keybindings_undo.tr(); } else if (key == redoCommand.key) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a618801894..bd054dabd3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -759,6 +759,7 @@ "alignLeft": "Align text left", "alignCenter": "Align text center", "alignRight": "Align text right", + "insertInlineMathEquation": "Insert inline math eqaution", "undo": "Undo", "redo": "Redo", "convertToParagraph": "Convert block to paragraph", From c760a1b1feb0e19607cce9525663ffcba1a87ae9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 27 Feb 2025 13:08:49 +0800 Subject: [PATCH 049/384] feat: support columns block in editor on desktop (#7402) * feat: support columns block in editor * feat: upgrade simple columns block * fix: build error * feat: add column width resizer * fix: drag visual border * fix: drag button position issue * feat: add rule to check if the column is empty * fix: flutter analyze * feat: add document rules to delete the columns if its children are empty * feat: support adding image in columns block * feat: integrate block actions in columns block * feat: support dragging to create a columns block * feat: drag a block into an existing columns block * feat: add delete columns and delete column rules * feat: dragging the block to the left side of another block to create a columns block * feat: support 2-4 columns block in slash menu * chore: disable debug flag in columns block * chore: update pubspec.yaml * chore: update translations and icons * fix: cloud integration test * fix: integration test --- .../document_option_actions_test.dart | 2 +- .../document_with_multi_image_block_test.dart | 4 +- .../mobile_selection_menu_item_widget.dart | 27 +++ .../document/application/document_bloc.dart | 44 +--- .../document/application/document_rules.dart | 118 +++++++++ .../presentation/editor_configuration.dart | 34 +++ .../actions/block_action_button.dart | 4 +- .../draggable_option_button.dart | 54 ++++- .../actions/drag_to_reorder/util.dart | 109 ++++++++- .../drag_to_reorder/visual_drag_area.dart | 29 +++ .../simple_column_block_component.dart | 194 +++++++++++++++ .../simple_column_block_width_resizer.dart | 124 ++++++++++ .../columns/simple_column_node_extension.dart | 34 +++ .../simple_columns_block_component.dart | 224 ++++++++++++++++++ .../simple_columns_block_constant.dart | 6 + .../copy_and_paste/custom_copy_command.dart | 11 +- .../presentation/editor_plugins/plugins.dart | 10 +- .../slash_menu_items/columns_item.dart | 92 +++++++ .../slash_menu_items/slash_menu_items.dart | 1 + .../slash_menu/slash_menu_items_builder.dart | 6 + .../document/presentation/editor_style.dart | 9 + .../lib/startup/tasks/rust_sdk.dart | 2 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../16x/slash_menu_icon_four_columns.svg | 3 + .../16x/slash_menu_icon_three_columns.svg | 3 + .../16x/slash_menu_icon_two_columns.svg | 3 + frontend/resources/translations/en.json | 5 +- 28 files changed, 1093 insertions(+), 65 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart create mode 100644 frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg create mode 100644 frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg create mode 100644 frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart index 0289fbe176..1bc9bd8f92 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart @@ -57,7 +57,7 @@ void main() { // move the checkbox to the child of the block at path [9] await tester.editor.dragBlock( [10], - const Offset(80, -30), + const Offset(120, -20), ); // wait for the move animation to complete diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart index 68ad7db7e5..d8b0784a39 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart @@ -48,7 +48,7 @@ void main() { await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( LocaleKeys.document_slashMenu_name_photoGallery.tr(), - offset: 80, + offset: 100, ); expect(find.byType(MultiImageBlockComponent), findsOneWidget); expect(find.byType(MultiImagePlaceholder), findsOneWidget); @@ -146,7 +146,7 @@ void main() { await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( LocaleKeys.document_slashMenu_name_photoGallery.tr(), - offset: 80, + offset: 100, ); expect(find.byType(MultiImageBlockComponent), findsOneWidget); expect(find.byType(MultiImagePlaceholder), findsOneWidget); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart index 1ed84a73d5..ae5b0b11ac 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart @@ -86,6 +86,15 @@ class MobileSelectionMenuStyle extends SelectionMenuStyle { required super.selectionMenuItemSelectedTextColor, required super.selectionMenuItemSelectedIconColor, required super.selectionMenuItemSelectedColor, + required super.selectionMenuUnselectedLabelColor, + required super.selectionMenuDividerColor, + required super.selectionMenuLinkBorderColor, + required super.selectionMenuInvalidLinkColor, + required super.selectionMenuButtonColor, + required super.selectionMenuButtonTextColor, + required super.selectionMenuButtonIconColor, + required super.selectionMenuButtonBorderColor, + required super.selectionMenuTabIndicatorColor, required this.selectionMenuItemRightIconColor, }); @@ -99,6 +108,15 @@ class MobileSelectionMenuStyle extends SelectionMenuStyle { selectionMenuItemRightIconColor: Color(0xB31E2022), selectionMenuItemSelectedTextColor: Color.fromARGB(255, 56, 91, 247), selectionMenuItemSelectedIconColor: Color.fromARGB(255, 56, 91, 247), + selectionMenuUnselectedLabelColor: Color(0xFF333333), + selectionMenuDividerColor: Color(0xFF00BCF0), + selectionMenuLinkBorderColor: Color(0xFF00BCF0), + selectionMenuInvalidLinkColor: Color(0xFFE53935), + selectionMenuButtonColor: Color(0xFF00BCF0), + selectionMenuButtonTextColor: Color(0xFF333333), + selectionMenuButtonIconColor: Color(0xFF333333), + selectionMenuButtonBorderColor: Color(0xFF00BCF0), + selectionMenuTabIndicatorColor: Color(0xFF00BCF0), ); static const MobileSelectionMenuStyle dark = MobileSelectionMenuStyle( @@ -109,5 +127,14 @@ class MobileSelectionMenuStyle extends SelectionMenuStyle { selectionMenuItemRightIconColor: Color(0xB3FFFFFF), selectionMenuItemSelectedTextColor: Color(0xFF131720), selectionMenuItemSelectedIconColor: Color(0xFF131720), + selectionMenuUnselectedLabelColor: Color(0xFFBBC3CD), + selectionMenuDividerColor: Color(0xFF3A3F44), + selectionMenuLinkBorderColor: Color(0xFF3A3F44), + selectionMenuInvalidLinkColor: Color(0xFFE53935), + selectionMenuButtonColor: Color(0xFF00BCF0), + selectionMenuButtonTextColor: Color(0xFFFFFFFF), + selectionMenuButtonIconColor: Color(0xFFFFFFFF), + selectionMenuButtonBorderColor: Color(0xFF00BCF0), + selectionMenuTabIndicatorColor: Color(0xFF00BCF0), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 641344cf27..268863664b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/application/document_awareness_metadat import 'package:appflowy/plugins/document/application/document_collab_adapter.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_listener.dart'; +import 'package:appflowy/plugins/document/application/document_rules.dart'; import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; @@ -26,13 +27,7 @@ import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' - show - AppFlowyEditorLogLevel, - EditorState, - Position, - Selection, - TransactionTime, - paragraphNode; + show AppFlowyEditorLogLevel, EditorState, TransactionTime; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -90,6 +85,8 @@ class DocumentBloc extends Bloc { documentService: _documentService, ); + late final DocumentRules _documentRules; + StreamSubscription? _transactionSubscription; bool isClosing = false; @@ -262,13 +259,14 @@ class DocumentBloc extends Bloc { final editorState = EditorState(document: document); _documentCollabAdapter = DocumentCollabAdapter(editorState, documentId); + _documentRules = DocumentRules(editorState: editorState); // subscribe to the document change from the editor _transactionSubscription = editorState.transactionStream.listen( - (event) async { - final time = event.$1; - final transaction = event.$2; - final options = event.$3; + (value) async { + final time = value.$1; + final transaction = value.$2; + final options = value.$3; if (time != TransactionTime.before) { return; } @@ -288,7 +286,7 @@ class DocumentBloc extends Bloc { await _transactionAdapter.apply(transaction, editorState); // check if the document is empty. - await _applyRules(); + await _documentRules.applyRules(value: value); if (enableDocumentInternalLog) { Log.debug( @@ -319,28 +317,6 @@ class DocumentBloc extends Bloc { return editorState; } - Future _applyRules() async { - await Future.wait([ - _ensureAtLeastOneParagraphExists(), - ]); - } - - Future _ensureAtLeastOneParagraphExists() async { - final editorState = state.editorState; - if (editorState == null) { - return; - } - final document = editorState.document; - if (document.root.children.isEmpty) { - final transaction = editorState.transaction; - transaction.insertNode([0], paragraphNode()); - transaction.afterSelection = Selection.collapsed( - Position(path: [0]), - ); - await editorState.apply(transaction); - } - } - Future _onDocumentStateUpdate(DocEventPB docEvent) async { if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) { return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart new file mode 100644 index 0000000000..230a5b8fa7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart @@ -0,0 +1,118 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +/// Apply rules to the document +/// +/// 1. ensure there is at least one paragraph in the document, otherwise the user will be blocked from typing +/// 2. remove columns block if its children are empty +class DocumentRules { + DocumentRules({ + required this.editorState, + }); + + final EditorState editorState; + + Future applyRules({ + required EditorTransactionValue value, + }) async { + await Future.wait([ + _ensureAtLeastOneParagraphExists(value: value), + _removeColumnIfItIsEmpty(value: value), + ]); + } + + Future _ensureAtLeastOneParagraphExists({ + required EditorTransactionValue value, + }) async { + final document = editorState.document; + if (document.root.children.isEmpty) { + final transaction = editorState.transaction; + transaction + ..insertNode([0], paragraphNode()) + ..afterSelection = Selection.collapsed( + Position(path: [0]), + ); + await editorState.apply(transaction); + } + } + + Future _removeColumnIfItIsEmpty({ + required EditorTransactionValue value, + }) async { + final transaction = value.$2; + final options = value.$3; + + if (options.inMemoryUpdate) { + return; + } + + for (final operation in transaction.operations) { + final deleteColumnsTransaction = editorState.transaction; + if (operation is DeleteOperation) { + final path = operation.path; + final column = editorState.document.nodeAtPath(path.parent); + if (column != null && column.type == SimpleColumnBlockKeys.type) { + // check if the column is empty + final children = column.children; + if (children.isEmpty) { + // delete the column or the columns + final columns = column.parent; + + if (columns != null && + columns.type == SimpleColumnsBlockKeys.type) { + final nonEmptyColumnCount = columns.children.fold( + 0, + (p, c) => c.children.isEmpty ? p : p + 1, + ); + + // Example: + // columns + // - column 1 + // - paragraph 1-1 + // - paragraph 1-2 + // - column 2 + // - paragraph 2 + // - column 3 + // - paragraph 3 + // + // case 1: delete the paragraph 3 from column 3. + // because there is only one child in column 3, we should delete the column 3 as well. + // the result should be: + // columns + // - column 1 + // - paragraph 1-1 + // - paragraph 1-2 + // - column 2 + // - paragraph 2 + // + // case 2: delete the paragraph 3 from column 3 and delete the paragraph 2 from column 2. + // in this case, there will be only one column left, so we should delete the columns block and flatten the children. + // the result should be: + // paragraph 1-1 + // paragraph 1-2 + + // if there is only one empty column left, delete the columns block and flatten the children + if (nonEmptyColumnCount <= 1) { + // move the children in columns out of the column + final children = columns.children + .map((e) => e.children) + .expand((e) => e) + .map((e) => e.deepCopy()) + .toList(); + deleteColumnsTransaction.insertNodes(columns.path, children); + deleteColumnsTransaction.deleteNode(columns); + } else { + // otherwise, delete the column + deleteColumnsTransaction.deleteNode(column); + } + } + } + } + } + + if (deleteColumnsTransaction.operations.isNotEmpty) { + await editorState.apply(deleteColumnsTransaction); + } + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 0bac8aae19..48e48e812d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -31,6 +31,10 @@ final Set supportSlashMenuNodeTypes = { SimpleTableBlockKeys.type, SimpleTableRowBlockKeys.type, SimpleTableCellBlockKeys.type, + + // Columns + SimpleColumnsBlockKeys.type, + SimpleColumnBlockKeys.type, }; /// Build the block component builders. @@ -372,6 +376,14 @@ Map _buildBlockComponentBuilderMap( configuration, alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths, ), + SimpleColumnsBlockKeys.type: _buildSimpleColumnsBlockComponentBuilder( + context, + configuration, + ), + SimpleColumnBlockKeys.type: _buildSimpleColumnBlockComponentBuilder( + context, + configuration, + ), }; final builders = { @@ -946,6 +958,28 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder( ); } +SimpleColumnsBlockComponentBuilder _buildSimpleColumnsBlockComponentBuilder( + BuildContext context, + BlockComponentConfiguration configuration, +) { + return SimpleColumnsBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => EdgeInsets.zero, + ), + ); +} + +SimpleColumnBlockComponentBuilder _buildSimpleColumnBlockComponentBuilder( + BuildContext context, + BlockComponentConfiguration configuration, +) { + return SimpleColumnBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => EdgeInsets.zero, + ), + ); +} + TextStyle _buildTextStyleInTableCell( BuildContext context, { required Node node, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart index aed2de9abf..3561bd33a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart @@ -48,8 +48,6 @@ class BlockActionButton extends StatelessWidget { ); } - return Align( - child: child, - ); + return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart index 8ab16aea4c..044f0ca595 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; @@ -9,7 +10,6 @@ import 'draggable_option_button_feedback.dart'; import 'option_button.dart'; // this flag is used to disable the tooltip of the block when it is dragged -@visibleForTesting ValueNotifier isDraggingAppFlowyEditorBlock = ValueNotifier(false); class DraggableOptionButton extends StatefulWidget { @@ -82,8 +82,34 @@ class _DraggableOptionButtonState extends State { void _onDragUpdate(DragUpdateDetails details) { isDraggingAppFlowyEditorBlock.value = true; + final offset = details.globalPosition; + widget.editorState.selectionService.renderDropTargetForOffset( - details.globalPosition, + offset, + interceptor: (context, targetNode) { + // if the cursor node is in a columns block or a column block, + // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. + final parentColumnNode = targetNode.parentColumn; + if (parentColumnNode != null) { + final position = getDragAreaPosition( + context, + targetNode, + offset, + ); + + if (position != null && position.$2 == HorizontalPosition.right) { + return parentColumnNode; + } + + if (position != null && + position.$2 == HorizontalPosition.left && + position.$1 == VerticalPosition.middle) { + return parentColumnNode; + } + } + + return targetNode; + }, builder: (context, data) { return VisualDragArea( editorState: widget.editorState, @@ -112,6 +138,30 @@ class _DraggableOptionButtonState extends State { final data = widget.editorState.selectionService.getDropTargetRenderData( globalPosition!, + interceptor: (context, targetNode) { + // if the cursor node is in a columns block or a column block, + // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. + final parentColumnNode = targetNode.parentColumn; + if (parentColumnNode != null) { + final position = getDragAreaPosition( + context, + targetNode, + globalPosition!, + ); + + if (position != null && position.$2 == HorizontalPosition.right) { + return parentColumnNode; + } + + if (position != null && + position.$2 == HorizontalPosition.left && + position.$1 == VerticalPosition.middle) { + return parentColumnNode; + } + } + + return targetNode; + }, ); dragToMoveNode( context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index fcafe9f8de..99b13add54 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -8,6 +8,15 @@ enum HorizontalPosition { left, center, right } enum VerticalPosition { top, middle, bottom } +List nodeTypesThatCanContainChildNode = [ + ParagraphBlockKeys.type, + BulletedListBlockKeys.type, + NumberedListBlockKeys.type, + QuoteBlockKeys.type, + TodoListBlockKeys.type, + ToggleListBlockKeys.type, +]; + Future dragToMoveNode( BuildContext context, { required Node node, @@ -35,17 +44,75 @@ Future dragToMoveNode( final (verticalPosition, horizontalPosition, _) = position; Path newPath = targetNode.path; + // if the horizontal position is right, creating a column block to contain the target node and the drag node + if (horizontalPosition == HorizontalPosition.right) { + // 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent + // 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node + final transaction = editorState.transaction; + final targetNodeParent = targetNode.parentColumnsBlock; + if (targetNodeParent != null) { + final columnNode = simpleColumnNode( + children: [node.deepCopy()], + ); + + transaction.insertNode(targetNode.path.next, columnNode); + transaction.deleteNode(node); + } else { + final columnsNode = simpleColumnsNode( + children: [ + simpleColumnNode(children: [targetNode.deepCopy()]), + simpleColumnNode(children: [node.deepCopy()]), + ], + ); + + transaction.insertNode(newPath, columnsNode); + transaction.deleteNode(targetNode); + transaction.deleteNode(node); + } + + if (transaction.operations.isNotEmpty) { + await editorState.apply(transaction); + } + return; + } else if (horizontalPosition == HorizontalPosition.left && + verticalPosition == VerticalPosition.middle) { + // 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent + // 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node + final transaction = editorState.transaction; + final targetNodeParent = targetNode.parentColumnsBlock; + if (targetNodeParent != null) { + final columnNode = simpleColumnNode( + children: [node.deepCopy()], + ); + + transaction.insertNode(targetNode.path.previous, columnNode); + transaction.deleteNode(node); + } else { + final columnsNode = simpleColumnsNode( + children: [ + simpleColumnNode(children: [node.deepCopy()]), + simpleColumnNode(children: [targetNode.deepCopy()]), + ], + ); + + transaction.insertNode(newPath, columnsNode); + transaction.deleteNode(targetNode); + transaction.deleteNode(node); + } + + if (transaction.operations.isNotEmpty) { + await editorState.apply(transaction); + } + return; + } // Determine the new path based on drop position // For VerticalPosition.top, we keep the target node's path if (verticalPosition == VerticalPosition.bottom) { if (horizontalPosition == HorizontalPosition.left) { newPath = newPath.next; - final node = editorState.document.nodeAtPath(newPath); - if (node == null) { - // if node is null, it means the node is the last one of the document. - newPath = targetNode.path; - } - } else { + } else if (horizontalPosition == HorizontalPosition.center && + nodeTypesThatCanContainChildNode.contains(targetNode.type)) { + // check if the target node can contain a child node newPath = newPath.child(0); } } @@ -103,22 +170,40 @@ Future dragToMoveNode( HorizontalPosition horizontalPosition = HorizontalPosition.left; VerticalPosition verticalPosition; - // Horizontal position + // | ----------------------------- block ----------------------------- | + // | 1. -- 88px --| 2. ---------------------------- | 3. ---- 1/5 ---- | + // 1. drag the node under the block as a sibling node + // 2. drag the node inside the block as a child node + // 3. create a column block to contain the node and the drag node + + // Horizontal position, please refer to the diagram above + // 88px is a hardcoded value, it can be changed based on the project's design if (dragOffset.dx < globalBlockRect.left + 88) { horizontalPosition = HorizontalPosition.left; - } else if (indentableBlockTypes.contains(dragTargetNode.type)) { - // For indentable blocks, it means the block can contain a child block. - // ignore the middle here, it's not used in this example + } else if (dragOffset.dx > globalBlockRect.right * 4.0 / 5.0) { horizontalPosition = HorizontalPosition.right; + } else if (nodeTypesThatCanContainChildNode.contains(dragTargetNode.type)) { + horizontalPosition = HorizontalPosition.center; } + // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is top + // | ----------------------------- block ----------------------------- | <- if the drag position is in this area, the vertical position is middle + // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is bottom + // Vertical position - if (dragOffset.dy < globalBlockRect.top + globalBlockRect.height / 2) { + final heightThird = globalBlockRect.height / 3; + if (dragOffset.dy < globalBlockRect.top + heightThird) { verticalPosition = VerticalPosition.top; + } else if (dragOffset.dy < globalBlockRect.top + heightThird * 2) { + verticalPosition = VerticalPosition.middle; } else { verticalPosition = VerticalPosition.bottom; } + debugPrint( + 'verticalPosition: $verticalPosition, horizontalPosition: $horizontalPosition', + ); + return (verticalPosition, horizontalPosition, globalBlockRect); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart index b4b4ffa904..2be8710a8a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart @@ -58,7 +58,36 @@ class VisualDragArea extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ); + // if the horizontal position is right, we need to show the indicator on the right side of the target node + // which represent moving the target node and drag node inside the column block. + if (horizontalPosition == HorizontalPosition.left && + verticalPosition == VerticalPosition.middle) { + return Positioned( + top: globalBlockRect.top, + height: globalBlockRect.height, + left: globalBlockRect.left + indicatorWidth, + child: Container( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + ); + } + if (horizontalPosition == HorizontalPosition.right) { + return Positioned( + top: globalBlockRect.top, + height: globalBlockRect.height, + left: globalBlockRect.right - 2, + child: Container( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + ); + } + + // If the horizontal position is center, we need to show two indicators + //which represent moving the block as the child of the target node. + if (horizontalPosition == HorizontalPosition.center) { const breakWidth = 22.0; const padding = 8.0; child = Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart new file mode 100644 index 0000000000..611f7ec67f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -0,0 +1,194 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +Node simpleColumnNode({ + List? children, + double? width, +}) { + return Node( + type: SimpleColumnBlockKeys.type, + children: children ?? [paragraphNode()], + attributes: { + SimpleColumnBlockKeys.width: width, + }, + ); +} + +class SimpleColumnBlockKeys { + const SimpleColumnBlockKeys._(); + + static const String type = 'simple_column'; + + static const String width = 'width'; +} + +class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder { + SimpleColumnBlockComponentBuilder({super.configuration}); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return SimpleColumnBlockComponent( + key: node.key, + node: node, + showActions: showActions(node), + configuration: configuration, + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), + ); + } + + @override + BlockComponentValidate get validate => (node) => node.children.isNotEmpty; +} + +class SimpleColumnBlockComponent extends BlockComponentStatefulWidget { + const SimpleColumnBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => + SimpleColumnBlockComponentState(); +} + +class SimpleColumnBlockComponentState extends State + with SelectableMixin, BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; + + final columnKey = GlobalKey(); + + late final EditorState editorState = context.read(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children.map( + (e) { + Widget child = IntrinsicHeight( + child: editorState.renderer.build(context, e), + ); + if (e.type == CustomImageBlockKeys.type) { + child = IntrinsicWidth(child: child); + } + if (SimpleColumnsBlockConstants.enableDebugBorder) { + child = DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Colors.blue, + ), + ), + child: child, + ); + } + return child; + }, + ).toList(), + ); + + child = Padding( + key: columnKey, + padding: padding, + child: child, + ); + + if (SimpleColumnsBlockConstants.enableDebugBorder) { + child = Container( + color: Colors.green.withValues( + alpha: 0.2, + ), + child: child, + ); + } + + // the column block does not support the block actions and selection + // because the column block is a layout wrapper, it does not have a content + return child; + } + + @override + Position start() => Position(path: widget.node.path); + + @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, + }) { + return getRectsInSelection(Selection.invalid()).first; + } + + @override + Rect? getCursorRectInPosition( + Position position, { + bool shiftWithBaseOffset = false, + }) { + final rects = getRectsInSelection( + Selection.collapsed(position), + shiftWithBaseOffset: shiftWithBaseOffset, + ); + return rects.firstOrNull; + } + + @override + List getRectsInSelection( + Selection selection, { + bool shiftWithBaseOffset = false, + }) { + if (_renderBox == null) { + return []; + } + final parentBox = context.findRenderObject(); + final renderBox = columnKey.currentContext?.findRenderObject(); + if (parentBox is RenderBox && renderBox is RenderBox) { + return [ + renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & + renderBox.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); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart new file mode 100644 index 0000000000..34d751520d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart @@ -0,0 +1,124 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +class ColumnBlockWidthResizer extends StatefulWidget { + const ColumnBlockWidthResizer({ + super.key, + required this.columnNode, + required this.editorState, + }); + + final Node columnNode; + final EditorState editorState; + + @override + State createState() => + _ColumnBlockWidthResizerState(); +} + +class _ColumnBlockWidthResizerState extends State { + bool isDragging = false; + + ValueNotifier isHovering = ValueNotifier(false); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => isHovering.value = true, + onExit: (_) { + // delay the hover state change to avoid flickering + Future.delayed(const Duration(milliseconds: 100), () { + if (!isDragging) { + isHovering.value = false; + } + }); + }, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: _onHorizontalDragStart, + onHorizontalDragUpdate: _onHorizontalDragUpdate, + onHorizontalDragEnd: _onHorizontalDragEnd, + onHorizontalDragCancel: _onHorizontalDragCancel, + child: ValueListenableBuilder( + valueListenable: isHovering, + builder: (context, isHovering, child) { + if (isDraggingAppFlowyEditorBlock.value) { + return SizedBox.shrink(); + } + return MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Container( + width: 2, + margin: EdgeInsets.symmetric(horizontal: 2), + color: isHovering + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + ), + ); + }, + ), + ), + ); + } + + void _onHorizontalDragStart(DragStartDetails details) { + isDragging = true; + } + + void _onHorizontalDragUpdate(DragUpdateDetails details) { + if (!isDragging) { + return; + } + + // update the column width in memory + final columnNode = widget.columnNode; + final rect = columnNode.rect; + final width = + columnNode.attributes[SimpleColumnBlockKeys.width] ?? rect.width; + final newWidth = width + details.delta.dx; + final transaction = widget.editorState.transaction; + transaction.updateNode(columnNode, { + ...columnNode.attributes, + SimpleColumnBlockKeys.width: newWidth.clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), + }); + final columnsNode = columnNode.parent; + if (columnsNode != null) { + transaction.updateNode(columnsNode, { + ...columnsNode.attributes, + ColumnsBlockKeys.columnCount: columnsNode.children.length, + }); + } + widget.editorState.apply( + transaction, + options: ApplyOptions(inMemoryUpdate: true), + ); + } + + void _onHorizontalDragEnd(DragEndDetails details) { + isHovering.value = false; + + if (!isDragging) { + return; + } + + // apply the transaction again to make sure the width is updated + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.columnNode, { + ...widget.columnNode.attributes, + }); + widget.editorState.apply(transaction); + + isDragging = false; + } + + void _onHorizontalDragCancel() { + isDragging = false; + isHovering.value = false; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart new file mode 100644 index 0000000000..3f4f73fd49 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +extension SimpleColumnNodeExtension on Node { + /// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock]. + Node? get parentColumnsBlock { + Node? currentNode = parent; + while (currentNode != null) { + if (currentNode.type == SimpleColumnsBlockKeys.type) { + return currentNode; + } + currentNode = currentNode.parent; + } + return null; + } + + /// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock]. + Node? get parentColumn { + Node? currentNode = parent; + while (currentNode != null) { + if (currentNode.type == SimpleColumnBlockKeys.type) { + return currentNode; + } + currentNode = currentNode.parent; + } + return null; + } + + /// Returns whether the current node is in a [SimpleColumnsBlock]. + bool get isInColumnsBlock => parentColumnsBlock != null; + + /// Returns whether the current node is in a [SimpleColumnBlock]. + bool get isInColumnBlock => parentColumn != null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart new file mode 100644 index 0000000000..cd7b2190cd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -0,0 +1,224 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +// if the children is not provided, it will create two columns by default. +// if the columnCount is provided, it will create the specified number of columns. +Node simpleColumnsNode({ + List? children, + int? columnCount, +}) { + columnCount ??= 2; + children ??= List.generate( + columnCount, + (index) => simpleColumnNode(children: [paragraphNode()]), + ); + + // check the type of children + for (final child in children) { + if (child.type != SimpleColumnBlockKeys.type) { + Log.error('the type of children must be column, but got ${child.type}'); + } + } + + return Node( + type: SimpleColumnsBlockKeys.type, + children: children, + ); +} + +class SimpleColumnsBlockKeys { + const SimpleColumnsBlockKeys._(); + + static const String type = 'simple_columns'; +} + +class SimpleColumnsBlockComponentBuilder extends BlockComponentBuilder { + SimpleColumnsBlockComponentBuilder({super.configuration}); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return ColumnsBlockComponent( + key: node.key, + node: node, + showActions: showActions(node), + configuration: configuration, + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), + ); + } + + @override + BlockComponentValidate get validate => (node) => node.children.isNotEmpty; +} + +class ColumnsBlockComponent extends BlockComponentStatefulWidget { + const ColumnsBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => ColumnsBlockComponentState(); +} + +class ColumnsBlockComponentState extends State + with SelectableMixin, BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; + + final columnsKey = GlobalKey(); + + late final EditorState editorState = context.read(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child = IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildChildren(), + ), + ); + + child = Padding( + key: columnsKey, + padding: padding, + child: child, + ); + + if (SimpleColumnsBlockConstants.enableDebugBorder) { + child = DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Colors.red, + width: 3.0, + ), + ), + child: child, + ); + } + + // the columns block does not support the block actions and selection + // because the columns block is a layout wrapper, it does not have a content + return child; + } + + List _buildChildren() { + final children = []; + for (var i = 0; i < node.children.length; i++) { + final childNode = node.children[i]; + final double? width = childNode.attributes[SimpleColumnBlockKeys.width]; + Widget child = editorState.renderer.build(context, childNode); + + if (width != null) { + child = SizedBox( + width: width.clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), + child: child, + ); + } else { + child = Expanded( + child: child, + ); + } + + children.add(child); + + if (i != node.children.length - 1) { + children.add( + ColumnBlockWidthResizer( + columnNode: childNode, + editorState: editorState, + ), + ); + } + } + return children; + } + + @override + Position start() => Position(path: widget.node.path); + + @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, + }) { + return getRectsInSelection(Selection.invalid()).first; + } + + @override + Rect? getCursorRectInPosition( + Position position, { + bool shiftWithBaseOffset = false, + }) { + final rects = getRectsInSelection( + Selection.collapsed(position), + shiftWithBaseOffset: shiftWithBaseOffset, + ); + return rects.firstOrNull; + } + + @override + List getRectsInSelection( + Selection selection, { + bool shiftWithBaseOffset = false, + }) { + if (_renderBox == null) { + return []; + } + final parentBox = context.findRenderObject(); + final renderBox = columnsKey.currentContext?.findRenderObject(); + if (parentBox is RenderBox && renderBox is RenderBox) { + return [ + renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & + renderBox.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); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart new file mode 100644 index 0000000000..977974a32e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart @@ -0,0 +1,6 @@ +class SimpleColumnsBlockConstants { + const SimpleColumnsBlockConstants._(); + + static const double minimumColumnWidth = 128; + static const bool enableDebugBorder = false; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart index 99211920fa..e56ccfc941 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart @@ -1,8 +1,7 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -98,7 +97,13 @@ Document _buildCopiedDocument( // if the node is a table cell, we will fetch its children instead. filteredNodes.addAll(node.children); } else if (node.type == SimpleTableRowBlockKeys.type) { - // if the node is a table row, we will fetch its children instead. + // if the node is a table row, we will fetch its children's children instead. + filteredNodes.addAll(node.children.expand((e) => e.children)); + } else if (node.type == SimpleColumnBlockKeys.type) { + // if the node is a column block, we will fetch its children instead. + filteredNodes.addAll(node.children); + } else if (node.type == SimpleColumnsBlockKeys.type) { + // if the node is a columns block, we will fetch its children's children instead. filteredNodes.addAll(node.children.expand((e) => e.children)); } else { filteredNodes.add(node); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index a7d63f2786..baf9a21b3a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -1,5 +1,8 @@ export 'actions/block_action_list.dart'; export 'actions/option/option_actions.dart'; +export 'ai/ai_writer_block_component.dart'; +export 'ai/ask_ai_block_component.dart'; +export 'ai/ask_ai_toolbar_item.dart'; export 'align_toolbar_item/align_toolbar_item.dart'; export 'base/backtick_character_command.dart'; export 'base/cover_title_command.dart'; @@ -8,6 +11,10 @@ export 'bulleted_list/bulleted_list_icon.dart'; export 'callout/callout_block_component.dart'; export 'code_block/code_block_language_selector.dart'; export 'code_block/code_block_menu_item.dart'; +export 'columns/simple_column_block_component.dart'; +export 'columns/simple_column_block_width_resizer.dart'; +export 'columns/simple_column_node_extension.dart'; +export 'columns/simple_columns_block_component.dart'; export 'context_menu/custom_context_menu.dart'; export 'copy_and_paste/custom_copy_command.dart'; export 'copy_and_paste/custom_cut_command.dart'; @@ -53,9 +60,6 @@ export 'mobile_toolbar_v3/toolbar_item_builder.dart'; export 'mobile_toolbar_v3/undo_redo_toolbar_item.dart'; export 'mobile_toolbar_v3/util.dart'; export 'numbered_list/numbered_list_icon.dart'; -export 'ai/ai_writer_block_component.dart'; -export 'ai/ask_ai_block_component.dart'; -export 'ai/ask_ai_toolbar_item.dart'; export 'outline/outline_block_component.dart'; export 'parsers/document_markdown_parsers.dart'; export 'parsers/markdown_parsers.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart new file mode 100644 index 0000000000..2deabd3e9e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart @@ -0,0 +1,92 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; + +final _baseKeywords = [ + 'columns', + 'column block', +]; + +final _twoColumnsKeywords = [ + ..._baseKeywords, + 'two columns', + '2 columns', +]; + +final _threeColumnsKeywords = [ + ..._baseKeywords, + 'three columns', + '3 columns', +]; + +final _fourColumnsKeywords = [ + ..._baseKeywords, + 'four columns', + '4 columns', +]; + +final _fiveColumnsKeywords = [ + ..._baseKeywords, + 'five columns', + '5 columns', +]; + +// 2 columns menu item +SelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node( + getName: () => LocaleKeys.document_slashMenu_name_twoColumns.tr(), + keywords: _twoColumnsKeywords, + nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 2), + replace: (_, node) => node.delta?.isEmpty ?? false, + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_two_columns_s, + isSelected: isSelected, + style: style, + ), +); + +// 3 columns menu item +SelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node( + getName: () => LocaleKeys.document_slashMenu_name_threeColumns.tr(), + keywords: _threeColumnsKeywords, + nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 3), + replace: (_, node) => node.delta?.isEmpty ?? false, + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_three_columns_s, + isSelected: isSelected, + style: style, + ), +); + +// 4 columns menu item +SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( + getName: () => LocaleKeys.document_slashMenu_name_fourColumns.tr(), + keywords: _fourColumnsKeywords, + nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 4), + replace: (_, node) => node.delta?.isEmpty ?? false, + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_four_columns_s, + isSelected: isSelected, + style: style, + ), +); + +// 5 columns menu item +SelectionMenuItem fiveColumnsSlashMenuItem = SelectionMenuItem.node( + getName: () => '5 Columns', + keywords: _fiveColumnsKeywords, + nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 5), + replace: (_, node) => node.delta?.isEmpty ?? false, + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_code_block_s, + isSelected: isSelected, + style: style, + ), +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart index ee76bc0ea3..223d8e742b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart @@ -2,6 +2,7 @@ export 'ai_writer_item.dart'; export 'bulleted_list_item.dart'; export 'callout_item.dart'; export 'code_block_item.dart'; +export 'columns_item.dart'; export 'database_items.dart'; export 'date_item.dart'; export 'divider_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart index 2cbc723a57..eb4ce5946b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart @@ -80,6 +80,12 @@ List _defaultSlashMenuItems({ // link to page linkToPageSlashMenuItem, + // columns + // 2-4 columns + twoColumnsSlashMenuItem, + threeColumnsSlashMenuItem, + fourColumnsSlashMenuItem, + // grid if (documentBloc != null) gridSlashMenuItem(documentBloc), referencedGridSlashMenuItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index c5df72dd0c..1f32c94cf4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -280,6 +280,15 @@ class EditorStyleCustomizer { selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface, selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface, selectionMenuItemSelectedColor: afThemeExtension.greyHover, + selectionMenuUnselectedLabelColor: afThemeExtension.onBackground, + selectionMenuDividerColor: afThemeExtension.greyHover, + selectionMenuLinkBorderColor: afThemeExtension.greyHover, + selectionMenuInvalidLinkColor: afThemeExtension.onBackground, + selectionMenuButtonColor: afThemeExtension.greyHover, + selectionMenuButtonTextColor: afThemeExtension.onBackground, + selectionMenuButtonIconColor: afThemeExtension.onBackground, + selectionMenuButtonBorderColor: afThemeExtension.greyHover, + selectionMenuTabIndicatorColor: afThemeExtension.greyHover, ); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index 58d6aacbc3..a0f5b0bafe 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -6,8 +6,8 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:flutter/foundation.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import '../startup.dart'; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index d08161d850..3174f2f1c5 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2745294" - resolved-ref: "2745294587cd3e2ff7ac0b6b91135c3ddcbb4274" + ref: "400083f" + resolved-ref: "400083fde6a1a77229416a55c50279f5f5b3f55b" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 39ed088126..8f72fcafc9 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -179,7 +179,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "2745294" + ref: "400083f" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg new file mode 100644 index 0000000000..aa4dbff160 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg new file mode 100644 index 0000000000..6eb3aeab2b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg new file mode 100644 index 0000000000..b3b3e55452 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index bd054dabd3..d5be5f22f7 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1736,7 +1736,10 @@ "aiWriter": "AI Writer", "dateOrReminder": "Date or Reminder", "photoGallery": "Photo Gallery", - "file": "File" + "file": "File", + "twoColumns": "Two Columns", + "threeColumns": "Three Columns", + "fourColumns": "Four Columns" }, "subPage": { "name": "Document", From db349519cfa67cbbf56137b7c5d15b9452443fc3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 27 Feb 2025 13:13:49 +0800 Subject: [PATCH 050/384] chore: bump version 0.8.5 (#7421) --- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 79862d18e7..6195e6a228 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.4" +APPFLOWY_VERSION = "0.8.5" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8f72fcafc9..111eac6e31 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.4 +version: 0.8.5 environment: flutter: ">=3.27.4" From 45b0233c21110f9a0850def84ceff9470556db3b Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Thu, 27 Feb 2025 06:14:48 +0100 Subject: [PATCH 051/384] chore: update Arabic translations (#7361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 31 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 4b4e112bce..1326fabad1 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -165,6 +165,15 @@ "csv": "CSV", "database": "قاعدة البيانات" }, + "emojiIconPicker": { + "iconUploader": { + "placeholderLeft": "اسحب وأفلِت ملفًا، وانقر فوقه ", + "placeholderUpload": "رفع", + "placeholderRight": "أو قم بلصق رابط الصورة.", + "dropToUpload": "إفلات ملف لتحميله", + "change": "تغير" + } + }, "disclosureAction": { "rename": "إعادة تسمية", "delete": "يمسح", @@ -178,7 +187,8 @@ "changeIcon": "تغيير الأيقونة", "collapseAllPages": "طي جميع الصفحات الفرعية", "movePageTo": "تحريك الصفحة إلى", - "move": "تحريك" + "move": "تحريك", + "lockPage": "إلغاء تأمين الصفحة" }, "blankPageTitle": "صفحة فارغة", "newPageText": "صفحة جديدة", @@ -576,7 +586,9 @@ "title": "تسجيل الدخول إلى الحساب", "loginLabel": "تسجيل الدخول", "logoutLabel": "تسجيل الخروج" - } + }, + "isUpToDate": "تم تحديث @:appName !", + "officialVersion": "الإصدار {version} (الإصدار الرسمي)" }, "workspacePage": { "menuLabel": "مساحة العمل", @@ -997,6 +1009,7 @@ "itemFour": "التعاون في الزمن الحقيقي", "itemFive": "تطبيق الجوال", "itemSix": "استجابات الذكاء الاصطناعي", + "itemSeven": "صور الذكاء الاصطناعي", "itemFileUpload": "رفع الملفات", "customNamespace": "مساحة اسم مخصصة", "tooltipSix": "تعني مدة الحياة أن عدد الاستجابات لا يتم إعادة ضبطه أبدًا", @@ -1011,6 +1024,7 @@ "itemFour": "نعم", "itemFive": "نعم", "itemSix": "10 مدى الحياة", + "itemSeven": "2 مدى الحياة", "itemFileUpload": "حتى 7 ميجا بايت", "intelligentSearch": "البحث الذكي" }, @@ -1021,6 +1035,7 @@ "itemFour": "نعم", "itemFive": "نعم", "itemSix": "غير محدود", + "itemSeven": "10 صور شهريا", "itemFileUpload": "غير محدود", "intelligentSearch": "البحث الذكي" }, @@ -1705,6 +1720,14 @@ "selectADocumentToLinkTo": "حدد مستندًا للارتباط به" }, "name": { + "textStyle": "نمط النص", + "list": "قائمة", + "toggle": "تبديل", + "fileAndMedia": "الملفات والوسائط", + "simpleTable": "جدول بسيط", + "visuals": "المرئيات", + "document": "وثيقة", + "advanced": "متقدم", "text": "نص", "heading1": "العنوان 1", "heading2": "العنوان 2", @@ -2193,11 +2216,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا" + "clickToOpen": "انقر لفتح السجل" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", From f73342d90295e64119a9023cd76eaf84cc1734c5 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 28 Feb 2025 15:18:21 +0800 Subject: [PATCH 052/384] fix: auto updater should not block the launch process (#7427) --- .../lib/shared/version_checker/version_checker.dart | 2 +- .../appflowy_flutter/lib/startup/tasks/auto_update_task.dart | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart b/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart index 1718fbb123..6e50a922a7 100644 --- a/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart +++ b/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart @@ -54,7 +54,7 @@ class VersionChecker { .nonNulls .firstWhereOrNull((e) => e.os == ApplicationInfo.os); } catch (e) { - Log.error('Failed to check for updates: $e'); + Log.info('Failed to check for updates: $e'); } return null; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart index 0a55558ea5..b666392544 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; @@ -26,7 +28,8 @@ class AutoUpdateTask extends LaunchTask { return; } - await _setupAutoUpdater(); + // don't use await here, because the auto updater is not a blocking operation + unawaited(_setupAutoUpdater()); ApplicationInfo.isCriticalUpdateNotifier.addListener( _showCriticalUpdateDialog, From adcac881a7cc96c7952c5d518b8804d4cff2279e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Mar 2025 09:57:36 +0800 Subject: [PATCH 053/384] fix: 0.8.5 launch review issues (#7430) * chore: replace two columns with 2 columns * fix: hide drag menu when the doc is locked * feat: add placeholder when editing the paragraph * fix: ingore tab shortcut in document title * feat: forward the video block to link preview block --- .../lib/plugins/document/document_page.dart | 5 ++ .../presentation/editor_configuration.dart | 49 +++++++++++-------- .../editor_plugins/header/cover_title.dart | 2 + .../presentation/editor_plugins/plugins.dart | 1 + ...mns_item.dart => simple_columns_item.dart} | 24 +++++++++ .../slash_menu_items/slash_menu_items.dart | 2 +- .../video/video_block_component.dart | 6 +++ frontend/resources/translations/en.json | 6 +-- 8 files changed, 71 insertions(+), 24 deletions(-) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/{columns_item.dart => simple_columns_item.dart} (84%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index bc25bf970d..83fc24c204 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; @@ -20,6 +21,7 @@ import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; @@ -188,6 +190,9 @@ class _DocumentPageState extends State ), header: buildCoverAndIcon(context, state), initialSelection: initialSelection, + placeholderText: (node) => node.type == ParagraphBlockKeys.type + ? LocaleKeys.editor_slashPlaceHolder.tr() + : '', ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 48e48e812d..5e5376d42b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -210,26 +210,30 @@ void _customBlockOptionActions( return ValueListenableBuilder( valueListenable: editorState.editableNotifier, builder: (_, editable, child) { - return Opacity( - opacity: editable ? 1.0 : 0.0, - child: Padding( - padding: EdgeInsets.only(top: top), - child: BlockActionList( - blockComponentContext: context, - blockComponentState: state, - editorState: editorState, - blockComponentBuilder: builders, - actions: actions, - showSlashMenu: slashMenuItemsBuilder != null - ? () => customAppFlowySlashCommand( - itemsBuilder: slashMenuItemsBuilder, - shouldInsertSlash: false, - deleteKeywordsByDefault: true, - style: styleCustomizer.selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: - supportSlashMenuNodeTypes, - ).handler.call(editorState) - : () {}, + return IgnorePointer( + ignoring: !editable, + child: Opacity( + opacity: editable ? 1.0 : 0.0, + child: Padding( + padding: EdgeInsets.only(top: top), + child: BlockActionList( + blockComponentContext: context, + blockComponentState: state, + editorState: editorState, + blockComponentBuilder: builders, + actions: actions, + showSlashMenu: slashMenuItemsBuilder != null + ? () => customAppFlowySlashCommand( + itemsBuilder: slashMenuItemsBuilder, + shouldInsertSlash: false, + deleteKeywordsByDefault: true, + style: + styleCustomizer.selectionMenuStyleBuilder(), + supportSlashMenuNodeTypes: + supportSlashMenuNodeTypes, + ).handler.call(editorState) + : () {}, + ), ), ), ); @@ -349,6 +353,11 @@ Map _buildBlockComponentBuilderMap( context, configuration, ), + // Flutter doesn't support the video widget, so we forward the video block to the link preview block + VideoBlockKeys.type: _buildLinkPreviewBlockComponentBuilder( + context, + configuration, + ), FileBlockKeys.type: _buildFileBlockComponentBuilder( context, configuration, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart index 23e117bbf4..097276a394 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart @@ -243,6 +243,8 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> { return _moveCursorToNextLine(event.logicalKey); } else if (event.logicalKey == LogicalKeyboardKey.escape) { return _exitEditing(); + } else if (event.logicalKey == LogicalKeyboardKey.tab) { + return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index baf9a21b3a..c37a85260e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -76,3 +76,4 @@ export 'table/table_option_action.dart'; export 'todo_list/todo_list_icon.dart'; export 'toggle/toggle_block_component.dart'; export 'toggle/toggle_block_shortcuts.dart'; +export 'video/video_block_component.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart similarity index 84% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart index 2deabd3e9e..4ffac6965e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/columns_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart @@ -47,6 +47,12 @@ SelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node( isSelected: isSelected, style: style, ), + updateSelection: (_, path, __, ___) { + return Selection.single( + path: path.child(0).child(0), + startOffset: 0, + ); + }, ); // 3 columns menu item @@ -61,6 +67,12 @@ SelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node( isSelected: isSelected, style: style, ), + updateSelection: (_, path, __, ___) { + return Selection.single( + path: path.child(0).child(0), + startOffset: 0, + ); + }, ); // 4 columns menu item @@ -75,6 +87,12 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( isSelected: isSelected, style: style, ), + updateSelection: (_, path, __, ___) { + return Selection.single( + path: path.child(0).child(0), + startOffset: 0, + ); + }, ); // 5 columns menu item @@ -89,4 +107,10 @@ SelectionMenuItem fiveColumnsSlashMenuItem = SelectionMenuItem.node( isSelected: isSelected, style: style, ), + updateSelection: (_, path, __, ___) { + return Selection.single( + path: path.child(0).child(0), + startOffset: 0, + ); + }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart index 223d8e742b..27be8e4f03 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart @@ -2,7 +2,6 @@ export 'ai_writer_item.dart'; export 'bulleted_list_item.dart'; export 'callout_item.dart'; export 'code_block_item.dart'; -export 'columns_item.dart'; export 'database_items.dart'; export 'date_item.dart'; export 'divider_item.dart'; @@ -16,6 +15,7 @@ export 'outline_item.dart'; export 'paragraph_item.dart'; export 'photo_gallery_item.dart'; export 'quote_item.dart'; +export 'simple_columns_item.dart'; export 'simple_table_item.dart'; export 'slash_menu_item_builder.dart'; export 'sub_page_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart new file mode 100644 index 0000000000..f41d4526ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart @@ -0,0 +1,6 @@ +class VideoBlockKeys { + const VideoBlockKeys._(); + + static const String type = 'video'; + static const String url = 'url'; +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index d5be5f22f7..b64fe35436 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1737,9 +1737,9 @@ "dateOrReminder": "Date or Reminder", "photoGallery": "Photo Gallery", "file": "File", - "twoColumns": "Two Columns", - "threeColumns": "Three Columns", - "fourColumns": "Four Columns" + "twoColumns": "2 Columns", + "threeColumns": "3 Columns", + "fourColumns": "4 Columns" }, "subPage": { "name": "Document", From 56a023c98a8d2d115bea325273b79d900678df84 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Mar 2025 11:20:58 +0800 Subject: [PATCH 054/384] fix: the locked hint is not visible when there's a cover (#7439) --- .../lib/mobile/presentation/base/mobile_view_page.dart | 4 ++-- .../lib/workspace/presentation/widgets/view_title_bar.dart | 7 ++++++- frontend/resources/flowy_icons/16x/lock_page_fill.svg | 4 ++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 frontend/resources/flowy_icons/16x/lock_page_fill.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 4d043410bf..792679daf1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -387,8 +387,8 @@ class _MobileViewPageState extends State { bottom: 4.0, ), child: FlowySvg( - FlowySvgs.lock_page_s, - color: const Color(0xFFD95A0B), + FlowySvgs.lock_page_fill_s, + blendMode: null, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index a1f2717d97..87452d560e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -400,6 +400,7 @@ class LockedPageStatus extends StatelessWidget { side: BorderSide(color: color), borderRadius: BorderRadius.circular(6), ), + color: Colors.white.withValues(alpha: 0.75), ), child: FlowyButton( useIntrinsicWidth: true, @@ -414,7 +415,10 @@ class LockedPageStatus extends StatelessWidget { fontSize: 12.0, ), hoverColor: color.withValues(alpha: 0.1), - leftIcon: FlowySvg(FlowySvgs.lock_page_s, color: color), + leftIcon: FlowySvg( + FlowySvgs.lock_page_fill_s, + blendMode: null, + ), onTap: () => context.read().add( const ViewLockStatusEvent.unlock(), ), @@ -436,6 +440,7 @@ class ReLockedPageStatus extends StatelessWidget { side: BorderSide(color: iconColor), borderRadius: BorderRadius.circular(6), ), + color: Colors.white.withValues(alpha: 0.75), ), child: FlowyButton( useIntrinsicWidth: true, diff --git a/frontend/resources/flowy_icons/16x/lock_page_fill.svg b/frontend/resources/flowy_icons/16x/lock_page_fill.svg new file mode 100644 index 0000000000..b2ed846e69 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/lock_page_fill.svg @@ -0,0 +1,4 @@ + + + + From c0dfec8b34da24b2986f7385d5c47557e1cfb267 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 3 Mar 2025 11:21:44 +0800 Subject: [PATCH 055/384] fix: potential issues with displaying CircleAvatar (#7432) --- .../presentation/widgets/user_avatar.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index adb3b1e454..347d95d01d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -90,15 +90,14 @@ class UserAvatar extends StatelessWidget { : null, ), child: ClipRRect( - borderRadius: Corners.s5Border, - child: CircleAvatar( - backgroundColor: Colors.transparent, - child: Image.network( - iconUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _buildEmptyAvatar(context), - ), + borderRadius: BorderRadius.circular(size / 2), + child: Image.network( + iconUrl, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), ), ), ), From 8ebd490260788bbceffb33b75e9cb8a76d60d54f Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Mar 2025 11:22:13 +0800 Subject: [PATCH 056/384] feat: support scrollable columns block (#7429) * feat: support scrollable columns block * fix: simple columns block issues on mobile * feat: hide drag menu when resizing the columns block --- .../presentation/editor_configuration.dart | 75 ++++++++++++------- .../simple_column_block_width_resizer.dart | 15 ++-- .../simple_columns_block_component.dart | 62 ++++++++------- .../slash_menu_items/simple_columns_item.dart | 42 ++++------- 4 files changed, 108 insertions(+), 86 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5e5376d42b..2d6a52a58d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -15,6 +15,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +/// A global configuration for the editor. +class EditorGlobalConfiguration { + /// Whether to enable the drag menu in the editor. + /// + /// Case 1, resizing the columns block in the desktop, then the drag menu will be disabled. + static ValueNotifier enableDragMenu = ValueNotifier(true); +} + /// The node types that support slash menu. final Set supportSlashMenuNodeTypes = { ParagraphBlockKeys.type, @@ -208,34 +216,39 @@ void _customBlockOptionActions( top += 2.0; } return ValueListenableBuilder( - valueListenable: editorState.editableNotifier, - builder: (_, editable, child) { - return IgnorePointer( - ignoring: !editable, - child: Opacity( - opacity: editable ? 1.0 : 0.0, - child: Padding( - padding: EdgeInsets.only(top: top), - child: BlockActionList( - blockComponentContext: context, - blockComponentState: state, - editorState: editorState, - blockComponentBuilder: builders, - actions: actions, - showSlashMenu: slashMenuItemsBuilder != null - ? () => customAppFlowySlashCommand( - itemsBuilder: slashMenuItemsBuilder, - shouldInsertSlash: false, - deleteKeywordsByDefault: true, - style: - styleCustomizer.selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: - supportSlashMenuNodeTypes, - ).handler.call(editorState) - : () {}, + valueListenable: EditorGlobalConfiguration.enableDragMenu, + builder: (_, enableDragMenu, child) { + return ValueListenableBuilder( + valueListenable: editorState.editableNotifier, + builder: (_, editable, child) { + return IgnorePointer( + ignoring: !editable, + child: Opacity( + opacity: editable && enableDragMenu ? 1.0 : 0.0, + child: Padding( + padding: EdgeInsets.only(top: top), + child: BlockActionList( + blockComponentContext: context, + blockComponentState: state, + editorState: editorState, + blockComponentBuilder: builders, + actions: actions, + showSlashMenu: slashMenuItemsBuilder != null + ? () => customAppFlowySlashCommand( + itemsBuilder: slashMenuItemsBuilder, + shouldInsertSlash: false, + deleteKeywordsByDefault: true, + style: styleCustomizer + .selectionMenuStyleBuilder(), + supportSlashMenuNodeTypes: + supportSlashMenuNodeTypes, + ).handler.call(editorState) + : () {}, + ), + ), ), - ), - ), + ); + }, ); }, ); @@ -973,7 +986,13 @@ SimpleColumnsBlockComponentBuilder _buildSimpleColumnsBlockComponentBuilder( ) { return SimpleColumnsBlockComponentBuilder( configuration: configuration.copyWith( - padding: (_) => EdgeInsets.zero, + padding: (node) { + if (UniversalPlatform.isMobile) { + return configuration.padding(node); + } + + return EdgeInsets.zero; + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart index 34d751520d..dad0dea132 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart @@ -1,11 +1,12 @@ +import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -class ColumnBlockWidthResizer extends StatefulWidget { - const ColumnBlockWidthResizer({ +class SimpleColumnBlockWidthResizer extends StatefulWidget { + const SimpleColumnBlockWidthResizer({ super.key, required this.columnNode, required this.editorState, @@ -15,11 +16,12 @@ class ColumnBlockWidthResizer extends StatefulWidget { final EditorState editorState; @override - State createState() => - _ColumnBlockWidthResizerState(); + State createState() => + _SimpleColumnBlockWidthResizerState(); } -class _ColumnBlockWidthResizerState extends State { +class _SimpleColumnBlockWidthResizerState + extends State { bool isDragging = false; ValueNotifier isHovering = ValueNotifier(false); @@ -66,6 +68,7 @@ class _ColumnBlockWidthResizerState extends State { void _onHorizontalDragStart(DragStartDetails details) { isDragging = true; + EditorGlobalConfiguration.enableDragMenu.value = false; } void _onHorizontalDragUpdate(DragUpdateDetails details) { @@ -102,6 +105,7 @@ class _ColumnBlockWidthResizerState extends State { void _onHorizontalDragEnd(DragEndDetails details) { isHovering.value = false; + EditorGlobalConfiguration.enableDragMenu.value = true; if (!isDragging) { return; @@ -120,5 +124,6 @@ class _ColumnBlockWidthResizerState extends State { void _onHorizontalDragCancel() { isDragging = false; isHovering.value = false; + EditorGlobalConfiguration.enableDragMenu.value = true; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index cd7b2190cd..849d5afd4c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -5,17 +5,22 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:universal_platform/universal_platform.dart'; // if the children is not provided, it will create two columns by default. // if the columnCount is provided, it will create the specified number of columns. Node simpleColumnsNode({ List? children, int? columnCount, + double? width, }) { columnCount ??= 2; children ??= List.generate( columnCount, - (index) => simpleColumnNode(children: [paragraphNode()]), + (index) => simpleColumnNode( + width: width, + children: [paragraphNode()], + ), ); // check the type of children @@ -95,14 +100,28 @@ class ColumnsBlockComponentState extends State @override Widget build(BuildContext context) { - Widget child = IntrinsicHeight( + Widget child = SingleChildScrollView( + scrollDirection: Axis.horizontal, child: Row( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: _buildChildren(), ), ); + if (UniversalPlatform.isDesktop) { + // only show the scrollbar on desktop + child = Scrollbar( + child: child, + ); + } + + child = Align( + alignment: Alignment.topLeft, + child: IntrinsicHeight( + child: child, + ), + ); + child = Padding( key: columnsKey, padding: padding, @@ -130,33 +149,26 @@ class ColumnsBlockComponentState extends State final children = []; for (var i = 0; i < node.children.length; i++) { final childNode = node.children[i]; - final double? width = childNode.attributes[SimpleColumnBlockKeys.width]; + final width = childNode.attributes[SimpleColumnBlockKeys.width] ?? + SimpleColumnsBlockConstants.minimumColumnWidth; Widget child = editorState.renderer.build(context, childNode); - if (width != null) { - child = SizedBox( - width: width.clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), - child: child, - ); - } else { - child = Expanded( - child: child, - ); - } + child = SizedBox( + width: width.clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), + child: child, + ); children.add(child); - if (i != node.children.length - 1) { - children.add( - ColumnBlockWidthResizer( - columnNode: childNode, - editorState: editorState, - ), - ); - } + children.add( + SimpleColumnBlockWidthResizer( + columnNode: childNode, + editorState: editorState, + ), + ); } return children; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart index 4ffac6965e..103cfface4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart @@ -29,17 +29,11 @@ final _fourColumnsKeywords = [ '4 columns', ]; -final _fiveColumnsKeywords = [ - ..._baseKeywords, - 'five columns', - '5 columns', -]; - // 2 columns menu item SelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node( getName: () => LocaleKeys.document_slashMenu_name_twoColumns.tr(), keywords: _twoColumnsKeywords, - nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 2), + nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 2), replace: (_, node) => node.delta?.isEmpty ?? false, nameBuilder: slashMenuItemNameBuilder, iconBuilder: (_, isSelected, style) => SelectableSvgWidget( @@ -59,7 +53,7 @@ SelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node( SelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node( getName: () => LocaleKeys.document_slashMenu_name_threeColumns.tr(), keywords: _threeColumnsKeywords, - nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 3), + nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 3), replace: (_, node) => node.delta?.isEmpty ?? false, nameBuilder: slashMenuItemNameBuilder, iconBuilder: (_, isSelected, style) => SelectableSvgWidget( @@ -79,7 +73,7 @@ SelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node( SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( getName: () => LocaleKeys.document_slashMenu_name_fourColumns.tr(), keywords: _fourColumnsKeywords, - nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 4), + nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 4), replace: (_, node) => node.delta?.isEmpty ?? false, nameBuilder: slashMenuItemNameBuilder, iconBuilder: (_, isSelected, style) => SelectableSvgWidget( @@ -95,22 +89,14 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( }, ); -// 5 columns menu item -SelectionMenuItem fiveColumnsSlashMenuItem = SelectionMenuItem.node( - getName: () => '5 Columns', - keywords: _fiveColumnsKeywords, - nodeBuilder: (_, __) => simpleColumnsNode(columnCount: 5), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_code_block_s, - isSelected: isSelected, - style: style, - ), - updateSelection: (_, path, __, ___) { - return Selection.single( - path: path.child(0).child(0), - startOffset: 0, - ); - }, -); +Node _buildColumnsNode(EditorState editorState, int columnCount) { + final selection = editorState.selection; + double? width; + if (selection != null) { + final parentNode = editorState.getNodeAtPath(selection.start.path); + if (parentNode != null) { + width = parentNode.rect.width / columnCount; + } + } + return simpleColumnsNode(columnCount: columnCount, width: width); +} From 249543d64f2713cb1fe5032459893692e861d5d3 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 3 Mar 2025 11:22:40 +0800 Subject: [PATCH 057/384] fix: using [[ to create subpage with error text (#7434) --- .../mobile/document/at_menu_test.dart | 15 +++++++++++++++ .../base/page_reference_commands.dart | 1 + 2 files changed, 16 insertions(+) diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart index 394cacffd6..2b348d3a2e 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart @@ -37,5 +37,20 @@ void main() { await tester.tap(actionWidgets.last); expect(find.byType(MentionPageBlock), findsOneWidget); }); + + testWidgets('create subpage with at menu', (tester) async { + await tester.launchInAnonymousMode(); + await tester.createNewDocumentOnMobile(title); + await tester.editor.tapLineOfEditorAt(0); + const subpageName = 'Subpage'; + await tester.ime.insertText('[[$subpageName'); + await tester.pumpAndSettle(); + final actionWidgets = find.byType(MobileInlineActionsWidget); + await tester.tapButton(actionWidgets.first); + final firstNode = + tester.editor.getCurrentEditorState().getNodeAtPath([0]); + assert(firstNode != null); + expect(firstNode!.delta?.toPlainText().contains('['), false); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart index 14402434b2..007e4ea298 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -120,6 +120,7 @@ Future inlinePageReferenceCommandHandler( editorState: editorState, service: service, initialResults: initialResults, + startCharAmount: previousChar != null ? 2 : 1, style: style, ) : InlineActionsMenu( From 2e17fb9dd3e9f5063cd9dced7a288913f4b1aa0c Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Mar 2025 13:14:29 +0800 Subject: [PATCH 058/384] fix: can't open the relation field in the linked database (#7441) --- .../desktop_grid_relation_cell.dart | 15 +++++--- .../desktop_row_detail_relation_cell.dart | 13 ++++--- .../cell_editor/relation_cell_editor.dart | 35 ++++++++++++------- .../widgets/row/relation_row_detail.dart | 2 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart index f4b8fb97f0..88d5fdf9d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart @@ -1,8 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -20,6 +21,7 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { RelationCellState state, PopoverController popoverController, ) { + final userWorkspaceBloc = context.read(); return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, @@ -27,8 +29,11 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { margin: EdgeInsets.zero, onClose: () => cellContainerNotifier.isFocus = false, popupBuilder: (context) { - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: userWorkspaceBloc), + BlocProvider.value(value: bloc), + ], child: const RelationCellEditor(), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart index b3096d49e4..89db702d22 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -19,6 +20,7 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { RelationCellState state, PopoverController popoverController, ) { + final userWorkspaceBloc = context.read(); return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, @@ -27,8 +29,11 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { asBarrier: true, onClose: () => cellContainerNotifier.isFocus = false, popupBuilder: (context) { - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: userWorkspaceBloc), + BlocProvider.value(value: bloc), + ], child: const RelationCellEditor(), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 3a739cc69c..e68e77cd97 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/row/relation_row_detail.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -112,8 +113,11 @@ class _RelationCellEditorContentState @override Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: bloc), + BlocProvider.value(value: context.read()), + ], child: BlocBuilder( buildWhen: (previous, current) => !listEquals(previous.filteredRows, current.filteredRows), @@ -316,13 +320,16 @@ class _SearchField extends StatelessWidget { FlowyOverlay.show( context: context, builder: (BuildContext overlayContext) { - return RelatedRowDetailPage( - databaseId: context - .read() - .state - .relatedDatabaseMeta! - .databaseId, - rowId: row.rowId, + return BlocProvider.value( + value: context.read(), + child: RelatedRowDetailPage( + databaseId: context + .read() + .state + .relatedDatabaseMeta! + .databaseId, + rowId: row.rowId, + ), ); }, ); @@ -391,13 +398,17 @@ class _RowListItem extends StatelessWidget { ), child: GestureDetector( onTap: () { + final userWorkspaceBloc = context.read(); if (isSelected) { FlowyOverlay.show( context: context, builder: (BuildContext overlayContext) { - return RelatedRowDetailPage( - databaseId: databaseId, - rowId: row.rowId, + return BlocProvider.value( + value: userWorkspaceBloc, + child: RelatedRowDetailPage( + databaseId: databaseId, + rowId: row.rowId, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart index e8d96734dd..1260641fdf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart @@ -23,7 +23,7 @@ class RelatedRowDetailPage extends StatelessWidget { initialRowId: rowId, ), child: BlocBuilder( - builder: (context, state) { + builder: (_, state) { return state.when( loading: () => const SizedBox.shrink(), ready: (databaseController, rowController) { From eacd7b250324c666a72b82d2f0b24fb15be50d63 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 3 Mar 2025 13:15:50 +0800 Subject: [PATCH 059/384] fix: error display when showing SnackBar with dialog (#7440) --- .../lib/plugins/database/widgets/row/row_detail.dart | 8 +++----- .../multi_image_block_component/multi_image_menu.dart | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart index 7b39335ec7..f6c1a87782 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart @@ -1,7 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; @@ -10,6 +6,7 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; +import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; @@ -17,11 +14,12 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import '../cell/editable_cell_builder.dart'; - import 'row_banner.dart'; import 'row_property.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart index 52bb41fd84..b98e05f231 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart @@ -12,6 +12,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; import 'package:appflowy_backend/log.dart'; @@ -216,9 +217,9 @@ class _MultiImageMenuState extends State { Clipboard.setData( ClipboardData(text: images[widget.indexNotifier.value].url), ); - showSnackBarMessage( + showToastNotification( context, - LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); } From fe6217bd82902aebb7576f127b5244a249b5d6ef Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:35:51 +0800 Subject: [PATCH 060/384] feat: ai writer block (#7406) * feat: ai writer block * test: fix integration tests * chore: add continue writing to slash menu * chore: focus issues during insertion * fix: explain button position * fix: gesture detection * fix: insert below * fix: undo * chore: improve writing toolbar item * chore: pass predefined format when using quick commands * fix: continue writing in an empty document or at the beginning of a document * fix: don't allow selecting text not in content * fix: related question not following predefined format --- .../document/document_ai_writer_test.dart | 4 +- .../shared/mock/mock_openai_repository.dart | 81 -- frontend/appflowy_flutter/lib/ai/ai.dart | 6 +- .../lib/ai/service/ai_client.dart | 34 - .../lib/ai/service/ai_entities.dart | 85 ++ .../service}/ai_prompt_input_bloc.dart | 31 +- .../lib/ai/service/appflowy_ai_service.dart | 174 ++-- .../lib/ai/service/error.dart | 2 +- .../lib/ai/service/openai_client.dart | 173 ---- .../lib/ai/service/text_completion.dart | 27 - .../lib/ai/widgets/loading_indicator.dart | 85 +- .../desktop_input_text_field.dart | 53 -- .../desktop_prompt_text_field.dart} | 305 ++++--- .../prompt_input/file_attachment_list.dart | 7 +- .../predefined_format_buttons.dart | 4 +- .../select_sources_bottom_sheet.dart | 106 +-- .../prompt_input/select_sources_menu.dart | 90 +- .../ai_chat/application/chat_bloc.dart | 15 +- .../ai_chat/application/chat_entity.dart | 87 -- .../chat_select_sources_cubit.dart | 5 +- .../lib/plugins/ai_chat/chat_page.dart | 209 +++-- .../chat_input/mobile_chat_input.dart | 148 +-- .../ai_chat/presentation/layout_define.dart | 9 - .../ai_change_format_bottom_sheet.dart | 2 +- .../message/ai_message_action_bar.dart | 7 +- .../message/ai_message_bubble.dart | 9 +- .../presentation/message/ai_text_message.dart | 20 +- .../message/user_message_bubble.dart | 8 +- .../editor_transaction_adapter.dart | 4 +- .../presentation/editor_configuration.dart | 13 +- .../document/presentation/editor_page.dart | 3 +- .../ai/ai_writer_block_component.dart | 840 +++++++++++------- .../ai/ai_writer_toolbar_item.dart | 241 +++++ .../ai/ask_ai_block_component.dart | 214 ----- .../ai/ask_ai_toolbar_item.dart | 146 --- .../ai_writer_block_operations.dart | 209 +++++ .../ai/operations/ai_writer_cubit.dart | 527 +++++++++++ .../ai/operations/ai_writer_entities.dart | 131 +++ .../ai_writer_node_extension.dart} | 24 +- .../ai/suggestion_action_bar.dart | 63 ++ .../ai/util/learn_more_action.dart | 8 - .../ai/widgets/ai_limit_dialog.dart | 13 - .../widgets/ai_writer_block_operations.dart | 112 --- .../ai/widgets/ai_writer_block_widgets.dart | 104 --- .../ai/widgets/ask_ai_action.dart | 14 - .../ai/widgets/ask_ai_action_bloc.dart | 299 ------- .../ai/widgets/ask_ai_block_widgets.dart | 112 --- .../ai/widgets/barrier_dialog.dart | 20 - .../ai/widgets/discard_dialog.dart | 28 - .../base/markdown_text_robot.dart | 254 ++++-- .../presentation/editor_plugins/plugins.dart | 3 +- .../slash_menu_items/ai_writer_item.dart | 80 +- .../slash_menu/slash_menu_items_builder.dart | 7 +- .../document/presentation/editor_style.dart | 51 +- .../lib/startup/deps_resolver.dart | 13 - .../ai_writer_test/ai_writer_bloc_test.dart | 411 +++++++++ .../ask_ai_test/ask_ai_action_bloc_test.dart | 331 ------- .../test/bloc_test/chat_test/util.dart | 8 - .../text_robot/markdown_text_robot_test.dart | 13 +- .../resources/flowy_icons/16x/ai_explain.svg | 5 + .../16x/ai_fix_spelling_grammar.svg | 3 + .../flowy_icons/16x/ai_improve_writing.svg | 6 + .../flowy_icons/16x/ai_make_longer.svg | 3 + .../flowy_icons/16x/ai_make_shorter.svg | 3 + .../resources/flowy_icons/16x/ai_sparks.svg | 5 +- .../flowy_icons/16x/ai_summarize.svg | 3 + .../16x/{ai_undo.svg => ai_try_again.svg} | 0 .../16x/suggestion_insert_below.svg | 3 + frontend/resources/translations/ar-SA.json | 1 - frontend/resources/translations/de-DE.json | 1 - frontend/resources/translations/en.json | 44 +- frontend/resources/translations/fr-FR.json | 1 - frontend/resources/translations/ja-JP.json | 2 - frontend/resources/translations/th-TH.json | 1 - frontend/resources/translations/tr-TR.json | 1 - frontend/resources/translations/vi-VN.json | 1 - frontend/resources/translations/zh-CN.json | 1 - frontend/resources/translations/zh-TW.json | 1 - .../event-integration-test/src/chat_event.rs | 1 + frontend/rust-lib/flowy-ai/src/completion.rs | 9 +- frontend/rust-lib/flowy-ai/src/entities.rs | 22 +- 81 files changed, 3271 insertions(+), 2928 deletions(-) delete mode 100644 frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart delete mode 100644 frontend/appflowy_flutter/lib/ai/service/ai_client.dart create mode 100644 frontend/appflowy_flutter/lib/ai/service/ai_entities.dart rename frontend/appflowy_flutter/lib/{plugins/ai_chat/application => ai/service}/ai_prompt_input_bloc.dart (84%) delete mode 100644 frontend/appflowy_flutter/lib/ai/service/openai_client.dart delete mode 100644 frontend/appflowy_flutter/lib/ai/service/text_completion.dart delete mode 100644 frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_input_text_field.dart rename frontend/appflowy_flutter/lib/{plugins/ai_chat/presentation/chat_input/desktop_chat_input.dart => ai/widgets/prompt_input/desktop_prompt_text_field.dart} (67%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_block_component.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/{util/ask_ai_node_extension.dart => operations/ai_writer_node_extension.dart} (74%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/learn_more_action.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_limit_dialog.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_operations.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_widgets.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_block_widgets.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/barrier_dialog.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/discard_dialog.dart create mode 100644 frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart delete mode 100644 frontend/appflowy_flutter/test/bloc_test/ask_ai_test/ask_ai_action_bloc_test.dart create mode 100644 frontend/resources/flowy_icons/16x/ai_explain.svg create mode 100644 frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg create mode 100644 frontend/resources/flowy_icons/16x/ai_improve_writing.svg create mode 100644 frontend/resources/flowy_icons/16x/ai_make_longer.svg create mode 100644 frontend/resources/flowy_icons/16x/ai_make_shorter.svg create mode 100644 frontend/resources/flowy_icons/16x/ai_summarize.svg rename frontend/resources/flowy_icons/16x/{ai_undo.svg => ai_try_again.svg} (100%) create mode 100644 frontend/resources/flowy_icons/16x/suggestion_insert_below.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart index 32737a23d1..f163608ccb 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart @@ -33,7 +33,7 @@ void main() { await tester.editor.tapSlashMenuItemWithName( LocaleKeys.document_slashMenu_name_aiWriter.tr(), ); - expect(find.byType(AIWriterBlockComponent), findsOneWidget); + expect(find.byType(AiWriterBlockComponent), findsOneWidget); // switch to another page await tester.openPage(Constants.gettingStartedPageName); @@ -41,7 +41,7 @@ void main() { await tester.openPage(pageName); // expect the ai writer block is not in the document - expect(find.byType(AIWriterBlockComponent), findsNothing); + expect(find.byType(AiWriterBlockComponent), findsNothing); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart deleted file mode 100644 index ec1891dea8..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/ai/service/error.dart'; -import 'package:appflowy/ai/service/openai_client.dart'; -import 'package:appflowy/ai/service/text_completion.dart'; -import 'package:http/http.dart' as http; -import 'package:mocktail/mocktail.dart'; - -class MyMockClient extends Mock implements http.Client { - @override - Future send(http.BaseRequest request) async { - final requestType = request.method; - final requestUri = request.url; - - if (requestType == 'POST' && - requestUri == OpenAIRequestType.textCompletion.uri) { - final responseHeaders = { - 'content-type': 'text/event-stream', - }; - final responseBody = Stream.fromIterable([ - utf8.encode( - '{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}', - ), - utf8.encode('\n'), - utf8.encode('[DONE]'), - ]); - - // Return a mocked response with the expected data - return http.StreamedResponse(responseBody, 200, headers: responseHeaders); - } - - // Return an error response for any other request - return http.StreamedResponse(const Stream.empty(), 404); - } -} - -class MockOpenAIRepository extends HttpOpenAIRepository { - MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient()); - - @override - Future getStreamedCompletions({ - required String prompt, - required Future Function() onStart, - required Future Function(TextCompletionResponse response) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - bool useAction = false, - }) async { - final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); - final response = await client.send(request); - - String previousSyntax = ''; - if (response.statusCode == 200) { - await for (final chunk in response.stream - .transform(const Utf8Decoder()) - .transform(const LineSplitter())) { - await onStart(); - final data = chunk.trim().split('data: '); - if (data[0] != '[DONE]') { - final response = TextCompletionResponse.fromJson( - json.decode(data[0]), - ); - if (response.choices.isNotEmpty) { - final text = response.choices.first.text; - if (text == previousSyntax && text == '\n') { - continue; - } - await onProcess(response); - previousSyntax = response.choices.first.text; - } - } else { - await onEnd(); - } - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/ai/ai.dart b/frontend/appflowy_flutter/lib/ai/ai.dart index a6fa49ef68..e3f52a8168 100644 --- a/frontend/appflowy_flutter/lib/ai/ai.dart +++ b/frontend/appflowy_flutter/lib/ai/ai.dart @@ -1,6 +1,10 @@ +export 'service/ai_entities.dart'; +export 'service/ai_prompt_input_bloc.dart'; +export 'service/appflowy_ai_service.dart'; +export 'service/error.dart'; export 'widgets/loading_indicator.dart'; export 'widgets/prompt_input/action_buttons.dart'; -export 'widgets/prompt_input/desktop_input_text_field.dart'; +export 'widgets/prompt_input/desktop_prompt_text_field.dart'; export 'widgets/prompt_input/file_attachment_list.dart'; export 'widgets/prompt_input/layout_define.dart'; export 'widgets/prompt_input/mention_page_bottom_sheet.dart'; diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_client.dart b/frontend/appflowy_flutter/lib/ai/service/ai_client.dart deleted file mode 100644 index a5c40c0569..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_client.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -import 'error.dart'; -import 'text_completion.dart'; - -abstract class AIRepository { - Future getStreamedCompletions({ - required String prompt, - required Future Function() onStart, - required Future Function(TextCompletionResponse response) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - bool useAction = false, - }); - - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }); - - Future, AIError>> generateImage({ - required String prompt, - int n = 1, - }); -} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart new file mode 100644 index 0000000000..b8592bc32b --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart @@ -0,0 +1,85 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; + +class PredefinedFormat extends Equatable { + const PredefinedFormat({ + required this.imageFormat, + required this.textFormat, + }); + + final ImageFormat imageFormat; + final TextFormat? textFormat; + + PredefinedFormatPB toPB() { + return PredefinedFormatPB( + imageFormat: switch (imageFormat) { + ImageFormat.text => ResponseImageFormatPB.TextOnly, + ImageFormat.image => ResponseImageFormatPB.ImageOnly, + ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage, + }, + textFormat: switch (textFormat) { + TextFormat.paragraph => ResponseTextFormatPB.Paragraph, + TextFormat.bulletList => ResponseTextFormatPB.BulletedList, + TextFormat.numberedList => ResponseTextFormatPB.NumberedList, + TextFormat.table => ResponseTextFormatPB.Table, + _ => null, + }, + ); + } + + @override + List get props => [imageFormat, textFormat]; +} + +enum ImageFormat { + text, + image, + textAndImage; + + bool get hasText => this == text || this == textAndImage; + + FlowySvgData get icon { + return switch (this) { + ImageFormat.text => FlowySvgs.ai_text_s, + ImageFormat.image => FlowySvgs.ai_image_s, + ImageFormat.textAndImage => FlowySvgs.ai_text_image_s, + }; + } + + String get i18n { + return switch (this) { + ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(), + ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(), + ImageFormat.textAndImage => + LocaleKeys.chat_changeFormat_textAndImage.tr(), + }; + } +} + +enum TextFormat { + paragraph, + bulletList, + numberedList, + table; + + FlowySvgData get icon { + return switch (this) { + TextFormat.paragraph => FlowySvgs.ai_paragraph_s, + TextFormat.bulletList => FlowySvgs.ai_list_s, + TextFormat.numberedList => FlowySvgs.ai_number_list_s, + TextFormat.table => FlowySvgs.ai_table_s, + }; + } + + String get i18n { + return switch (this) { + TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(), + TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(), + TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(), + TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(), + }; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart similarity index 84% rename from frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_prompt_input_bloc.dart rename to frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index fdbcbb4cc0..e80b196db6 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -10,12 +10,15 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'ai_entities.dart'; + part 'ai_prompt_input_bloc.freezed.dart'; class AIPromptInputBloc extends Bloc { - AIPromptInputBloc() - : _listener = LocalLLMListener(), - super(AIPromptInputState.initial()) { + AIPromptInputBloc({ + required PredefinedFormat? predefinedFormat, + }) : _listener = LocalLLMListener(), + super(AIPromptInputState.initial(predefinedFormat)) { _dispatch(); _startListening(); _init(); @@ -65,6 +68,16 @@ class AIPromptInputBloc extends Bloc { ), ); }, + toggleShowPredefinedFormat: () { + emit( + state.copyWith( + showPredefinedFormats: !state.showPredefinedFormats, + ), + ); + }, + updatePredefinedFormat: (format) { + emit(state.copyWith(predefinedFormat: format)); + }, attachFile: (filePath, fileName) { final newFile = ChatFile.fromFilePath(filePath); if (newFile != null) { @@ -152,6 +165,11 @@ class AIPromptInputEvent with _$AIPromptInputEvent { const factory AIPromptInputEvent.updatePluginState( LocalAIPluginStatePB chatState, ) = _UpdatePluginState; + const factory AIPromptInputEvent.toggleShowPredefinedFormat() = + _ToggleShowPredefinedFormat; + const factory AIPromptInputEvent.updatePredefinedFormat( + PredefinedFormat format, + ) = _UpdatePredefinedFormat; const factory AIPromptInputEvent.attachFile( String filePath, String fileName, @@ -167,14 +185,19 @@ class AIPromptInputState with _$AIPromptInputState { const factory AIPromptInputState({ required AIType aiType, required bool supportChatWithFile, + required bool showPredefinedFormats, + required PredefinedFormat? predefinedFormat, required LocalAIChatPB? chatState, required List attachedFiles, required List mentionedPages, }) = _AIPromptInputState; - factory AIPromptInputState.initial() => const AIPromptInputState( + factory AIPromptInputState.initial(PredefinedFormat? format) => + AIPromptInputState( aiType: AIType.appflowyAI, supportChatWithFile: false, + showPredefinedFormats: format != null, + predefinedFormat: format, chatState: null, attachedFiles: [], mentionedPages: [], diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 540e9f2804..2709b1d59c 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -3,137 +3,109 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart' as fixnum; -import 'ai_client.dart'; +import 'ai_entities.dart'; import 'error.dart'; -import 'text_completion.dart'; -enum AskAIAction { - summarize, - fixSpelling, - improveWriting, - makeItLonger; - - String get toInstruction => switch (this) { - summarize => 'Tl;dr', - fixSpelling => 'Correct this to standard English:', - improveWriting => 'Rewrite this in your own words:', - makeItLonger => 'Make this text longer:', - }; - - String prompt(String input) => switch (this) { - summarize => '$input\n\n$toInstruction', - _ => "$toInstruction\n\n$input", - }; - - static AskAIAction from(int index) => switch (index) { - 0 => summarize, - 1 => fixSpelling, - 2 => improveWriting, - 3 => makeItLonger, - _ => fixSpelling - }; - - String get name => switch (this) { - summarize => LocaleKeys.document_plugins_smartEditSummarize.tr(), - fixSpelling => LocaleKeys.document_plugins_smartEditFixSpelling.tr(), - improveWriting => - LocaleKeys.document_plugins_smartEditImproveWriting.tr(), - makeItLonger => LocaleKeys.document_plugins_smartEditMakeLonger.tr(), - }; +abstract class AIRepository { + Future streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }); } class AppFlowyAIService implements AIRepository { @override - Future, AIError>> generateImage({ - required String prompt, - int n = 1, - }) { - throw UnimplementedError(); - } - - @override - Future getStreamedCompletions({ - required String prompt, - required Future Function() onStart, - required Future Function(TextCompletionResponse response) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - bool useAction = false, - }) { - throw UnimplementedError(); - } - - @override - Future streamCompletion({ + Future<(String, CompletionStream)?> streamCompletion({ String? objectId, required String text, + PredefinedFormat? format, + List sourceIds = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, }) async { - final stream = CompletionStream( - onStart, - onProcess, - onEnd, - onError, + final stream = AppFlowyCompletionStream( + onStart: onStart, + onProcess: onProcess, + onEnd: onEnd, + onError: onError, ); - final List ragIds = []; - if (objectId != null) { - ragIds.add(objectId); - } final payload = CompleteTextPB( text: text, completionType: completionType, + format: format?.toPB(), streamPort: fixnum.Int64(stream.nativePort), - objectId: objectId ?? "", - ragIds: ragIds, + objectId: objectId ?? '', + ragIds: [ + if (objectId != null) objectId, + ...sourceIds, + ].unique(), ); - // ignore: unawaited_futures - AIEventCompleteText(payload).send(); - return stream; + return AIEventCompleteText(payload).send().fold( + (task) => (task.taskId, stream), + (error) { + Log.error(error); + return null; + }, + ); } } -CompletionTypePB completionTypeFromInt(AskAIAction action) { - switch (action) { - case AskAIAction.summarize: - return CompletionTypePB.MakeShorter; - case AskAIAction.fixSpelling: - return CompletionTypePB.SpellingAndGrammar; - case AskAIAction.improveWriting: - return CompletionTypePB.ImproveWriting; - case AskAIAction.makeItLonger: - return CompletionTypePB.MakeLonger; - } +abstract class CompletionStream { + CompletionStream({ + required this.onStart, + required this.onProcess, + required this.onEnd, + required this.onError, + }); + + final Future Function() onStart; + final Future Function(String text) onProcess; + final Future Function() onEnd; + final void Function(AIError error) onError; } -class CompletionStream { - CompletionStream( - Future Function() onStart, - Future Function(String text) onProcess, - Future Function() onEnd, - void Function(AIError error) onError, - ) { +class AppFlowyCompletionStream extends CompletionStream { + AppFlowyCompletionStream({ + required super.onStart, + required super.onProcess, + required super.onEnd, + required super.onError, + }) { + _startListening(); + } + + final RawReceivePort _port = RawReceivePort(); + final StreamController _controller = StreamController.broadcast(); + late StreamSubscription _subscription; + int get nativePort => _port.sendPort.nativePort; + + void _startListening() { _port.handler = _controller.add; _subscription = _controller.stream.listen( (event) async { if (event == "AI_RESPONSE_LIMIT") { onError( AIError( - message: LocaleKeys.sideBar_aiResponseLimit.tr(), + message: LocaleKeys.ai_textLimitReachedDescription.tr(), code: AIErrorCode.aiResponseLimitExceeded, ), ); @@ -142,7 +114,7 @@ class CompletionStream { if (event == "AI_IMAGE_RESPONSE_LIMIT") { onError( AIError( - message: LocaleKeys.sideBar_aiImageResponseLimit.tr(), + message: LocaleKeys.ai_imageLimitReachedDescription.tr(), code: AIErrorCode.aiImageResponseLimitExceeded, ), ); @@ -153,6 +125,7 @@ class CompletionStream { onError( AIError( message: msg, + code: AIErrorCode.other, ), ); } @@ -170,26 +143,17 @@ class CompletionStream { } if (event.startsWith("error:")) { - onError(AIError(message: event.substring(6))); + onError( + AIError(message: event.substring(6), code: AIErrorCode.other), + ); } }, ); } - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - int get nativePort => _port.sendPort.nativePort; - Future dispose() async { await _controller.close(); await _subscription.cancel(); _port.close(); } - - StreamSubscription listen( - void Function(String event)? onData, - ) { - return _controller.stream.listen(onData); - } } diff --git a/frontend/appflowy_flutter/lib/ai/service/error.dart b/frontend/appflowy_flutter/lib/ai/service/error.dart index 866a383e0b..0c98e83172 100644 --- a/frontend/appflowy_flutter/lib/ai/service/error.dart +++ b/frontend/appflowy_flutter/lib/ai/service/error.dart @@ -7,7 +7,7 @@ part 'error.g.dart'; class AIError with _$AIError { const factory AIError({ required String message, - @Default(AIErrorCode.other) AIErrorCode code, + required AIErrorCode code, }) = _AIError; factory AIError.fromJson(Map json) => diff --git a/frontend/appflowy_flutter/lib/ai/service/openai_client.dart b/frontend/appflowy_flutter/lib/ai/service/openai_client.dart deleted file mode 100644 index f29024a914..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/openai_client.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:http/http.dart' as http; - -import 'ai_client.dart'; -import 'error.dart'; -import 'text_completion.dart'; - -enum OpenAIRequestType { - textCompletion, - textEdit, - imageGenerations; - - Uri get uri { - switch (this) { - case OpenAIRequestType.textCompletion: - return Uri.parse('https://api.openai.com/v1/completions'); - case OpenAIRequestType.textEdit: - return Uri.parse('https://api.openai.com/v1/chat/completions'); - case OpenAIRequestType.imageGenerations: - return Uri.parse('https://api.openai.com/v1/images/generations'); - } - } -} - -class HttpOpenAIRepository implements AIRepository { - const HttpOpenAIRepository({ - required this.client, - required this.apiKey, - }); - - final http.Client client; - final String apiKey; - - Map get headers => { - 'Authorization': 'Bearer $apiKey', - 'Content-Type': 'application/json', - }; - - @override - Future getStreamedCompletions({ - required String prompt, - required Future Function() onStart, - required Future Function(TextCompletionResponse response) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - bool useAction = false, - }) async { - final parameters = { - 'model': 'gpt-3.5-turbo-instruct', - 'prompt': prompt, - 'suffix': suffix, - 'max_tokens': maxTokens, - 'temperature': temperature, - 'stream': true, - }; - - final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); - request.headers.addAll(headers); - request.body = jsonEncode(parameters); - - final response = await client.send(request); - - // NEED TO REFACTOR. - // WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE? - // AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE? - int syntax = 0; - var previousSyntax = ''; - if (response.statusCode == 200) { - await for (final chunk in response.stream - .transform(const Utf8Decoder()) - .transform(const LineSplitter())) { - syntax += 1; - if (!useAction) { - if (syntax == 3) { - await onStart(); - continue; - } else if (syntax < 3) { - continue; - } - } else { - if (syntax == 2) { - await onStart(); - continue; - } else if (syntax < 2) { - continue; - } - } - final data = chunk.trim().split('data: '); - if (data.length > 1) { - if (data[1] != '[DONE]') { - final response = TextCompletionResponse.fromJson( - json.decode(data[1]), - ); - if (response.choices.isNotEmpty) { - final text = response.choices.first.text; - if (text == previousSyntax && text == '\n') { - continue; - } - await onProcess(response); - previousSyntax = response.choices.first.text; - } - } else { - await onEnd(); - } - } - } - } else { - final body = await response.stream.bytesToString(); - onError( - AIError.fromJson(json.decode(body)['error']), - ); - } - return; - } - - @override - Future, AIError>> 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 FlowyResult.success(urls); - } else { - return FlowyResult.failure( - AIError.fromJson(json.decode(response.body)['error']), - ); - } - } catch (error) { - return FlowyResult.failure(AIError(message: error.toString())); - } - } - - @override - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }) { - throw UnimplementedError(); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/service/text_completion.dart b/frontend/appflowy_flutter/lib/ai/service/text_completion.dart deleted file mode 100644 index 4c22325588..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/text_completion.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'text_completion.freezed.dart'; -part 'text_completion.g.dart'; - -@freezed -class TextCompletionChoice with _$TextCompletionChoice { - factory TextCompletionChoice({ - required String text, - required int index, - // ignore: invalid_annotation_target - @JsonKey(name: 'finish_reason') String? finishReason, - }) = _TextCompletionChoice; - - factory TextCompletionChoice.fromJson(Map json) => - _$TextCompletionChoiceFromJson(json); -} - -@freezed -class TextCompletionResponse with _$TextCompletionResponse { - const factory TextCompletionResponse({ - required List choices, - }) = _TextCompletionResponse; - - factory TextCompletionResponse.fromJson(Map json) => - _$TextCompletionResponseFromJson(json); -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart index f1c5cf0cfb..e21ea95ac1 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart @@ -16,51 +16,48 @@ class AILoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - height: 20, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: FlowyText( - text, - color: Theme.of(context).hintColor, - ), + return SizedBox( + height: 20, + child: SeparatedRow( + separatorBuilder: () => const HSpace(4), + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: FlowyText( + text, + color: Theme.of(context).hintColor, ), - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], - ), + ), + buildDot(const Color(0xFF9327FF)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(duration: slice * 2, begin: 0, end: 0), + buildDot(const Color(0xFFFB006D)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: 0) + .then() + .slideY(begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(begin: 0, end: 0), + buildDot(const Color(0xFFFFCE00)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice * 2, begin: 0, end: 0) + .then() + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_input_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_input_text_field.dart deleted file mode 100644 index ed95da1509..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_input_text_field.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:flutter/material.dart'; - -import 'mentioned_page_text_span.dart'; - -class PromptInputTextField extends StatelessWidget { - const PromptInputTextField({ - super.key, - required this.cubit, - required this.textController, - required this.textFieldFocusNode, - required this.contentPadding, - this.hintText = "", - }); - - final ChatInputControlCubit cubit; - final TextEditingController textController; - final FocusNode textFieldFocusNode; - final EdgeInsetsGeometry contentPadding; - final String hintText; - - @override - Widget build(BuildContext context) { - return ExtendedTextField( - controller: textController, - focusNode: textFieldFocusNode, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: contentPadding, - hintText: hintText, - hintStyle: AIChatUILayout.inputHintTextStyle(context), - isCollapsed: true, - isDense: true, - ), - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - minLines: 1, - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - specialTextSpanBuilder: PromptInputTextSpanBuilder( - inputControlCubit: cubit, - specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_chat_input.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart similarity index 67% rename from frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_chat_input.dart rename to frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 375fb79e12..e2eeb80434 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_chat_input.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -1,41 +1,42 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../layout_define.dart'; - -class DesktopChatInput extends StatefulWidget { - const DesktopChatInput({ +class DesktopPromptInput extends StatefulWidget { + const DesktopPromptInput({ super.key, - required this.chatId, required this.isStreaming, required this.onStopStreaming, required this.onSubmitted, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, + this.hideDecoration = false, }); - final String chatId; final bool isStreaming; final void Function() onStopStreaming; final void Function(String, PredefinedFormat?, Map) onSubmitted; + final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; + final bool hideDecoration; @override - State createState() => _DesktopChatInputState(); + State createState() => _DesktopPromptInputState(); } -class _DesktopChatInputState extends State { +class _DesktopPromptInputState extends State { final textFieldKey = GlobalKey(); final layerLink = LayerLink(); final overlayController = OverlayPortalController(); @@ -43,11 +44,6 @@ class _DesktopChatInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); - bool showPredefinedFormatSection = true; - PredefinedFormat predefinedFormat = const PredefinedFormat( - imageFormat: ImageFormat.text, - textFormat: TextFormat.bulletList, - ); late SendButtonState sendButtonState; bool isComposing = false; @@ -57,13 +53,15 @@ class _DesktopChatInputState extends State { textController.addListener(handleTextControllerChanged); - // refresh border color on focus change and hide menu when lost focus focusNode.addListener( - () => setState(() { - if (!focusNode.hasFocus) { - cancelMentionPage(); + () { + if (!widget.hideDecoration) { + setState(() {}); // refresh border color } - }), + if (!focusNode.hasFocus) { + cancelMentionPage(); // hide menu when lost focus + } + }, ); updateSendButtonState(); @@ -112,15 +110,7 @@ class _DesktopChatInputState extends State { ); }, child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: focusNode.hasFocus - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - width: focusNode.hasFocus ? 1.5 : 1.0, - ), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), + decoration: decoration(context), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -132,7 +122,6 @@ class _DesktopChatInputState extends State { ), child: TextFieldTapRegion( child: PromptInputFile( - chatId: widget.chatId, onDeleted: (file) => context .read() .add(AIPromptInputEvent.removeFile(file)), @@ -140,53 +129,63 @@ class _DesktopChatInputState extends State { ), ), const VSpace(4.0), - Stack( - children: [ - Container( - constraints: getTextFieldConstraints(), - child: inputTextField(), - ), - if (showPredefinedFormatSection) - Positioned.fill( - bottom: null, - child: TextFieldTapRegion( - child: Padding( - padding: - const EdgeInsetsDirectional.only(start: 8.0), - child: ChangeFormatBar( - predefinedFormat: predefinedFormat, - spacing: 4.0, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); - }, + BlocBuilder( + builder: (context, state) { + return Stack( + children: [ + ConstrainedBox( + constraints: getTextFieldConstraints( + state.showPredefinedFormats, + ), + child: inputTextField(), + ), + if (state.showPredefinedFormats) + Positioned.fill( + bottom: null, + child: TextFieldTapRegion( + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 8.0, + ), + child: ChangeFormatBar( + predefinedFormat: state.predefinedFormat, + spacing: 4.0, + onSelectPredefinedFormat: (format) => + context.read().add( + AIPromptInputEvent + .updatePredefinedFormat( + format, + ), + ), + ), + ), + ), + ), + Positioned.fill( + top: null, + child: TextFieldTapRegion( + child: _PromptBottomActions( + showPredefinedFormats: + state.showPredefinedFormats, + onTogglePredefinedFormatSection: () => + context.read().add( + AIPromptInputEvent + .toggleShowPredefinedFormat(), + ), + onStartMention: startMentionPageFromButton, + sendButtonState: sendButtonState, + onSendPressed: handleSend, + onStopStreaming: widget.onStopStreaming, + selectedSourcesNotifier: + widget.selectedSourcesNotifier, + onUpdateSelectedSources: + widget.onUpdateSelectedSources, ), ), ), - ), - Positioned.fill( - top: null, - child: TextFieldTapRegion( - child: _PromptBottomActions( - textController: textController, - overlayController: overlayController, - focusNode: focusNode, - showPredefinedFormats: showPredefinedFormatSection, - predefinedFormat: predefinedFormat, - onTogglePredefinedFormatSection: () { - setState(() { - showPredefinedFormatSection = - !showPredefinedFormatSection; - }); - }, - sendButtonState: sendButtonState, - onSendPressed: handleSend, - onStopStreaming: widget.onStopStreaming, - onUpdateSelectedSources: - widget.onUpdateSelectedSources, - ), - ), - ), - ], + ], + ); + }, ), ], ), @@ -196,6 +195,40 @@ class _DesktopChatInputState extends State { ); } + BoxDecoration decoration(BuildContext context) { + if (widget.hideDecoration) { + return BoxDecoration(); + } + return BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border.all( + color: focusNode.hasFocus + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + width: focusNode.hasFocus ? 1.5 : 1.0, + ), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + ); + } + + void startMentionPageFromButton() { + if (overlayController.isShowing) { + return; + } + if (!focusNode.hasFocus) { + focusNode.requestFocus(); + } + textController.text += '@'; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + context + .read() + .startSearching(textController.value); + overlayController.show(); + } + }); + } + void cancelMentionPage() { if (overlayController.isShowing) { inputControlCubit.reset(); @@ -228,9 +261,13 @@ class _DesktopChatInputState extends State { // get the attached files and mentioned pages final metadata = context.read().consumeMetadata(); + final bloc = context.read(); + final showPredefinedFormats = bloc.state.showPredefinedFormats; + final predefinedFormat = bloc.state.predefinedFormat; + widget.onSubmitted( trimmedText, - showPredefinedFormatSection ? predefinedFormat : null, + showPredefinedFormats ? predefinedFormat : null, metadata, ); } @@ -330,18 +367,6 @@ class _DesktopChatInputState extends State { overlayController.hide(); } - BoxConstraints getTextFieldConstraints() { - double minHeight = DesktopAIPromptSizes.textFieldMinHeight + - DesktopAIPromptSizes.actionBarSendButtonSize + - DesktopAIChatSizes.inputActionBarMargin.vertical; - double maxHeight = 300; - if (showPredefinedFormatSection) { - minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; - maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; - } - return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); - } - Widget inputTextField() { return Shortcuts( shortcuts: buildShortcuts(), @@ -356,7 +381,8 @@ class _DesktopChatInputState extends State { cubit: inputControlCubit, textController: textController, textFieldFocusNode: focusNode, - contentPadding: calculateContentPadding(), + contentPadding: + calculateContentPadding(state.showPredefinedFormats), hintText: switch (state.aiType) { AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() @@ -369,8 +395,20 @@ class _DesktopChatInputState extends State { ); } - EdgeInsetsGeometry calculateContentPadding() { - final top = showPredefinedFormatSection + BoxConstraints getTextFieldConstraints(bool showPredefinedFormats) { + double minHeight = DesktopAIPromptSizes.textFieldMinHeight + + DesktopAIPromptSizes.actionBarSendButtonSize + + DesktopAIChatSizes.inputActionBarMargin.vertical; + double maxHeight = 300; + if (showPredefinedFormats) { + minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; + maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; + } + return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); + } + + EdgeInsetsGeometry calculateContentPadding(bool showPredefinedFormats) { + final top = showPredefinedFormats ? DesktopAIPromptSizes.predefinedFormatButtonHeight : 0.0; final bottom = DesktopAIPromptSizes.actionBarSendButtonSize + @@ -451,29 +489,80 @@ class _FocusNextItemIntent extends Intent { const _FocusNextItemIntent(); } +class PromptInputTextField extends StatelessWidget { + const PromptInputTextField({ + super.key, + required this.cubit, + required this.textController, + required this.textFieldFocusNode, + required this.contentPadding, + this.hintText = "", + }); + + final ChatInputControlCubit cubit; + final TextEditingController textController; + final FocusNode textFieldFocusNode; + final EdgeInsetsGeometry contentPadding; + final String hintText; + + @override + Widget build(BuildContext context) { + return ExtendedTextField( + controller: textController, + focusNode: textFieldFocusNode, + decoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: contentPadding, + hintText: hintText, + hintStyle: inputHintTextStyle(context), + isCollapsed: true, + isDense: true, + ), + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + minLines: 1, + maxLines: null, + style: Theme.of(context).textTheme.bodyMedium, + specialTextSpanBuilder: PromptInputTextSpanBuilder( + inputControlCubit: cubit, + specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + TextStyle? inputHintTextStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).isLightMode + ? const Color(0xFFBDC2C8) + : const Color(0xFF3C3E51), + ); + } +} + class _PromptBottomActions extends StatelessWidget { const _PromptBottomActions({ - required this.textController, - required this.overlayController, - required this.focusNode, required this.sendButtonState, - required this.predefinedFormat, - required this.onTogglePredefinedFormatSection, required this.showPredefinedFormats, + required this.onTogglePredefinedFormatSection, + required this.onStartMention, required this.onSendPressed, required this.onStopStreaming, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, }); - final TextEditingController textController; - final OverlayPortalController overlayController; - final FocusNode focusNode; final bool showPredefinedFormats; - final PredefinedFormat predefinedFormat; final void Function() onTogglePredefinedFormatSection; + final void Function() onStartMention; final SendButtonState sendButtonState; final void Function() onSendPressed; final void Function() onStopStreaming; + final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; @override @@ -520,7 +609,6 @@ class _PromptBottomActions extends StatelessWidget { Widget _predefinedFormatButton() { return PromptInputDesktopToggleFormatButton( showFormatBar: showPredefinedFormats, - predefinedFormat: predefinedFormat, onTap: onTogglePredefinedFormatSection, ); } @@ -528,6 +616,7 @@ class _PromptBottomActions extends StatelessWidget { Widget _selectSourcesButton(BuildContext context) { return PromptInputDesktopSelectSourcesButton( onUpdateSelectedSources: onUpdateSelectedSources, + selectedSourcesNotifier: selectedSourcesNotifier, ); } @@ -535,21 +624,7 @@ class _PromptBottomActions extends StatelessWidget { // return PromptInputMentionButton( // iconSize: DesktopAIPromptSizes.actionBarIconSize, // buttonSize: DesktopAIPromptSizes.actionBarButtonSize, - // onTap: () { - // if (overlayController.isShowing) { - // return; - // } - // if (!focusNode.hasFocus) { - // focusNode.requestFocus(); - // } - // textController.text += '@'; - // Future.delayed(Duration.zero, () { - // context - // .read() - // .startSearching(textController.value); - // overlayController.show(); - // }); - // }, + // onTap: onStartMention, // ); // } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart index fddde4dc69..cd68205506 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart'; +import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -13,11 +13,9 @@ import 'layout_define.dart'; class PromptInputFile extends StatelessWidget { const PromptInputFile({ super.key, - required this.chatId, required this.onDeleted, }); - final String chatId; final void Function(ChatFile) onDeleted; @override @@ -37,7 +35,6 @@ class PromptInputFile extends StatelessWidget { ), itemCount: files.length, itemBuilder: (context, index) => ChatFilePreview( - chatId: chatId, file: files[index], onDeleted: () => onDeleted(files[index]), ), @@ -49,13 +46,11 @@ class PromptInputFile extends StatelessWidget { class ChatFilePreview extends StatefulWidget { const ChatFilePreview({ - required this.chatId, required this.file, required this.onDeleted, super.key, }); - final String chatId; final ChatFile file; final VoidCallback onDeleted; diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart index 13db6a0669..189882ae4d 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -1,24 +1,22 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.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'; import 'package:universal_platform/universal_platform.dart'; +import '../../service/ai_entities.dart'; import 'layout_define.dart'; class PromptInputDesktopToggleFormatButton extends StatelessWidget { const PromptInputDesktopToggleFormatButton({ super.key, required this.showFormatBar, - required this.predefinedFormat, required this.onTap, }); final bool showFormatBar; - final PredefinedFormat predefinedFormat; final VoidCallback onTap; @override diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart index b69a2210f2..1f1b2ddf4c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart @@ -2,7 +2,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; @@ -19,9 +18,11 @@ import 'select_sources_menu.dart'; class PromptInputMobileSelectSourcesButton extends StatefulWidget { const PromptInputMobileSelectSourcesButton({ super.key, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, }); + final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; @override @@ -36,15 +37,15 @@ class _PromptInputMobileSelectSourcesButtonState @override void initState() { super.initState(); + widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged); WidgetsBinding.instance.addPostFrameCallback((_) { - cubit.updateSelectedSources( - context.read().state.selectedSourceIds, - ); + onSelectedSourcesChanged(); }); } @override void dispose() { + widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged); cubit.close(); super.dispose(); } @@ -70,56 +71,49 @@ class _PromptInputMobileSelectSourcesButtonState ], child: BlocBuilder( builder: (context, state) { - return BlocListener( - listener: (context, state) { - cubit - ..updateSelectedSources(state.selectedSourceIds) - ..updateSelectedStatus(); - }, - child: FlowyButton( - margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6), - expandText: false, - text: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.ai_page_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(20.0), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(10), - ), - ], - ), - onTap: () async { - context - .read() - .refreshSources(state.spaces, state.currentSpace); - await showMobileBottomSheet( - context, - backgroundColor: Theme.of(context).colorScheme.surface, - maxChildSize: 0.98, - enableDraggableScrollable: true, - scrollableWidgetBuilder: (_, scrollController) { - return Expanded( - child: BlocProvider.value( - value: cubit, - child: _MobileSelectSourcesSheetBody( - scrollController: scrollController, - ), - ), - ); - }, - builder: (context) => const SizedBox.shrink(), - ); - if (context.mounted) { - widget.onUpdateSelectedSources(cubit.selectedSourceIds); - } - }, + return FlowyButton( + margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6), + expandText: false, + text: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.ai_page_s, + color: Theme.of(context).iconTheme.color, + size: const Size.square(20.0), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(10), + ), + ], ), + onTap: () async { + context + .read() + .refreshSources(state.spaces, state.currentSpace); + await showMobileBottomSheet( + context, + backgroundColor: Theme.of(context).colorScheme.surface, + maxChildSize: 0.98, + enableDraggableScrollable: true, + scrollableWidgetBuilder: (_, scrollController) { + return Expanded( + child: BlocProvider.value( + value: cubit, + child: _MobileSelectSourcesSheetBody( + scrollController: scrollController, + ), + ), + ); + }, + builder: (context) => const SizedBox.shrink(), + ); + if (context.mounted) { + widget.onUpdateSelectedSources(cubit.selectedSourceIds); + } + }, ); }, ), @@ -127,6 +121,12 @@ class _PromptInputMobileSelectSourcesButtonState }, ); } + + void onSelectedSourcesChanged() { + cubit + ..updateSelectedSources(widget.selectedSourcesNotifier.value) + ..updateSelectedStatus(); + } } class _MobileSelectSourcesSheetBody extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart index df521256d8..d7c920c49c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -24,9 +24,11 @@ import 'mention_page_menu.dart'; class PromptInputDesktopSelectSourcesButton extends StatefulWidget { const PromptInputDesktopSelectSourcesButton({ super.key, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, }); + final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; @override @@ -42,15 +44,15 @@ class _PromptInputDesktopSelectSourcesButtonState @override void initState() { super.initState(); + widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged); WidgetsBinding.instance.addPostFrameCallback((_) { - cubit.updateSelectedSources( - context.read().state.selectedSourceIds, - ); + onSelectedSourcesChanged(); }); } @override void dispose() { + widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged); cubit.close(); super.dispose(); } @@ -76,51 +78,53 @@ class _PromptInputDesktopSelectSourcesButtonState ], child: BlocBuilder( builder: (context, state) { - return BlocListener( - listener: (context, state) { - cubit - ..updateSelectedSources(state.selectedSourceIds) - ..updateSelectedStatus(); + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(320, 380)), + offset: const Offset(0.0, -10.0), + direction: PopoverDirection.topWithCenterAligned, + margin: EdgeInsets.zero, + controller: popoverController, + onOpen: () { + context + .read() + .refreshSources(state.spaces, state.currentSpace); }, - child: AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(320, 380)), - offset: const Offset(0.0, -10.0), - direction: PopoverDirection.topWithCenterAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () { - context - .read() - .refreshSources(state.spaces, state.currentSpace); - }, - onClose: () { - widget.onUpdateSelectedSources(cubit.selectedSourceIds); - context - .read() - .refreshSources(state.spaces, state.currentSpace); - }, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: const _PopoverContent(), - ); - }, - child: _IndicatorButton( - onTap: () => popoverController.show(), - ), + onClose: () { + widget.onUpdateSelectedSources(cubit.selectedSourceIds); + context + .read() + .refreshSources(state.spaces, state.currentSpace); + }, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: const _PopoverContent(), + ); + }, + child: _IndicatorButton( + selectedSourcesNotifier: widget.selectedSourcesNotifier, + onTap: () => popoverController.show(), ), ); }, ), ); } + + void onSelectedSourcesChanged() { + cubit + ..updateSelectedSources(widget.selectedSourcesNotifier.value) + ..updateSelectedStatus(); + } } class _IndicatorButton extends StatelessWidget { const _IndicatorButton({ + required this.selectedSourcesNotifier, required this.onTap, }); + final ValueNotifier> selectedSourcesNotifier; final VoidCallback onTap; @override @@ -144,11 +148,19 @@ class _IndicatorButton extends StatelessWidget { color: Theme.of(context).iconTheme.color, ), const HSpace(2.0), - BlocBuilder( - builder: (context, state) { + ValueListenableBuilder( + valueListenable: selectedSourcesNotifier, + builder: (context, selectedSourceIds, _) { + final documentId = + context.read()?.documentId; + final label = documentId != null && + selectedSourceIds.length == 1 && + selectedSourceIds[0] == documentId + ? LocaleKeys.chat_currentPage.tr() + : selectedSourceIds.length.toString(); return FlowyText( - state.selectedSourceIds.length.toString(), - fontSize: 14, + label, + fontSize: 12, figmaLineHeight: 16, color: Theme.of(context).hintColor, ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 71bf7f63bf..e7aca346e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/util/int64_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -10,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -28,6 +30,7 @@ class ChatBloc extends Bloc { required this.userId, }) : chatController = InMemoryChatController(), listener = ChatMessageListener(chatId: chatId), + selectedSourcesNotifier = ValueNotifier([]), super(ChatState.initial()) { _startListening(); _dispatch(); @@ -38,7 +41,7 @@ class ChatBloc extends Bloc { final String chatId; final String userId; final ChatMessageListener listener; - + final ValueNotifier> selectedSourcesNotifier; final ChatController chatController; /// The last streaming message id @@ -68,7 +71,7 @@ class ChatBloc extends Bloc { await listener.stop(); final request = ViewIdPB(value: chatId); unawaited(FolderEventCloseView(request).send()); - + selectedSourcesNotifier.dispose(); return super.close(); } @@ -251,12 +254,10 @@ class ChatBloc extends Bloc { ); }, didReceiveChatSettings: (settings) { - emit( - state.copyWith(selectedSourceIds: settings.ragIds), - ); + selectedSourcesNotifier.value = settings.ragIds; }, updateSelectedSources: (selectedSourcesIds) async { - emit(state.copyWith(selectedSourceIds: selectedSourcesIds)); + selectedSourcesNotifier.value = [...selectedSourcesIds]; final payload = UpdateChatSettingsPB( chatId: ChatId(value: chatId), @@ -665,14 +666,12 @@ class ChatEvent with _$ChatEvent { @freezed class ChatState with _$ChatState { const factory ChatState({ - required List selectedSourceIds, required LoadChatMessageStatus loadingState, required PromptResponseState promptResponseState, required bool clearErrorMessages, }) = _ChatState; factory ChatState.initial() => const ChatState( - selectedSourceIds: [], loadingState: LoadChatMessageStatus.loading, promptResponseState: PromptResponseState.ready, clearErrorMessages: false, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart index 7b16c597cd..41e2a6946d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart @@ -1,11 +1,8 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:path/path.dart' as path; @@ -140,87 +137,3 @@ enum LoadChatMessageStatus { loadingRemote, ready, } - -class PredefinedFormat extends Equatable { - const PredefinedFormat({ - required this.imageFormat, - required this.textFormat, - }); - - const PredefinedFormat.auto() - : imageFormat = ImageFormat.text, - textFormat = TextFormat.paragraph; - - final ImageFormat imageFormat; - final TextFormat? textFormat; - - PredefinedFormatPB toPB() { - return PredefinedFormatPB( - imageFormat: switch (imageFormat) { - ImageFormat.text => ResponseImageFormatPB.TextOnly, - ImageFormat.image => ResponseImageFormatPB.ImageOnly, - ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage, - }, - textFormat: switch (textFormat) { - TextFormat.paragraph => ResponseTextFormatPB.Paragraph, - TextFormat.bulletList => ResponseTextFormatPB.BulletedList, - TextFormat.numberedList => ResponseTextFormatPB.NumberedList, - TextFormat.table => ResponseTextFormatPB.Table, - _ => null, - }, - ); - } - - @override - List get props => [imageFormat, textFormat]; -} - -enum ImageFormat { - text, - image, - textAndImage; - - bool get hasText => this == text || this == textAndImage; - - FlowySvgData get icon { - return switch (this) { - ImageFormat.text => FlowySvgs.ai_text_s, - ImageFormat.image => FlowySvgs.ai_image_s, - ImageFormat.textAndImage => FlowySvgs.ai_text_image_s, - }; - } - - String get i18n { - return switch (this) { - ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(), - ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(), - ImageFormat.textAndImage => - LocaleKeys.chat_changeFormat_textAndImage.tr(), - }; - } -} - -enum TextFormat { - paragraph, - bulletList, - numberedList, - table; - - FlowySvgData get icon { - return switch (this) { - TextFormat.paragraph => FlowySvgs.ai_paragraph_s, - TextFormat.bulletList => FlowySvgs.ai_list_s, - TextFormat.numberedList => FlowySvgs.ai_number_list_s, - TextFormat.table => FlowySvgs.ai_table_s, - }; - } - - String get i18n { - return switch (this) { - TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(), - TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(), - TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(), - TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(), - }; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart index 7a9f790e63..76cbd69cdf 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart @@ -113,9 +113,8 @@ class ChatSettingsCubit extends Cubit { String filter = ''; void updateSelectedSources(List newSelectedSourceIds) { - selectedSourceIds - ..clear - ..addAll(newSelectedSourceIds); + selectedSourceIds.clear(); + selectedSourceIds.addAll(newSelectedSourceIds); } void refreshSources(List spaceViews, ViewPB? currentSpace) async { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 551a210d00..f7f22a3c93 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; @@ -19,14 +20,12 @@ import 'package:string_validator/string_validator.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'application/ai_prompt_input_bloc.dart'; import 'application/chat_bloc.dart'; import 'application/chat_entity.dart'; import 'application/chat_member_bloc.dart'; import 'application/chat_select_message_bloc.dart'; import 'application/chat_message_stream.dart'; import 'presentation/animated_chat_list.dart'; -import 'presentation/chat_input/desktop_chat_input.dart'; import 'presentation/chat_input/mobile_chat_input.dart'; import 'presentation/chat_related_question.dart'; import 'presentation/chat_welcome_page.dart'; @@ -71,7 +70,14 @@ class AIChatPage extends StatelessWidget { ), /// [AIPromptInputBloc] is used to handle the user prompt - BlocProvider(create: (_) => AIPromptInputBloc()), + BlocProvider( + create: (_) => AIPromptInputBloc( + predefinedFormat: PredefinedFormat( + imageFormat: ImageFormat.text, + textFormat: TextFormat.bulletList, + ), + ), + ), BlocProvider(create: (_) => ChatMemberBloc()), ], child: Builder( @@ -146,7 +152,7 @@ class _ChatContentPage extends StatelessWidget { ), ), _wrapConstraints( - _builtInput(context), + _Input(view: view), ), ], ), @@ -184,9 +190,16 @@ class _ChatContentPage extends StatelessWidget { return RelatedQuestionList( relatedQuestions: message.metadata!['questions'], onQuestionSelected: (question) { - context - .read() - .add(ChatEvent.sendMessage(message: question)); + final bloc = context.read(); + final showPredefinedFormats = bloc.state.showPredefinedFormats; + final predefinedFormat = bloc.state.predefinedFormat; + + context.read().add( + ChatEvent.sendMessage( + message: question, + format: showPredefinedFormats ? predefinedFormat : null, + ), + ); }, ); } @@ -278,7 +291,16 @@ class _ChatContentPage extends StatelessWidget { return ChatWelcomePage( userProfile: userProfile, onSelectedQuestion: (question) { - bloc.add(ChatEvent.sendMessage(message: question)); + final aiPromptInputBloc = context.read(); + final showPredefinedFormats = + aiPromptInputBloc.state.showPredefinedFormats; + final predefinedFormat = aiPromptInputBloc.state.predefinedFormat; + bloc.add( + ChatEvent.sendMessage( + message: question, + format: showPredefinedFormats ? predefinedFormat : null, + ), + ); }, ); } @@ -300,86 +322,6 @@ class _ChatContentPage extends StatelessWidget { ); } - Widget _builtInput(BuildContext context) { - return BlocSelector( - selector: (state) => state.isSelectingMessages, - builder: (context, isSelectingMessages) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 150), - transitionBuilder: (child, animation) { - return SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ); - }, - child: isSelectingMessages - ? const SizedBox.shrink() - : Padding( - padding: AIChatUILayout.safeAreaInsets(context), - child: BlocSelector( - selector: (state) { - return state.promptResponseState == - PromptResponseState.ready; - }, - builder: (context, canSendMessage) { - final chatBloc = context.read(); - - return UniversalPlatform.isDesktop - ? DesktopChatInput( - chatId: view.id, - isStreaming: !canSendMessage, - onStopStreaming: () { - chatBloc.add(const ChatEvent.stopStream()); - }, - onSubmitted: (text, format, metadata) { - chatBloc.add( - ChatEvent.sendMessage( - message: text, - format: format, - metadata: metadata, - ), - ); - }, - onUpdateSelectedSources: (ids) { - chatBloc.add( - ChatEvent.updateSelectedSources( - selectedSourcesIds: ids, - ), - ); - }, - ) - : MobileChatInput( - chatId: view.id, - isStreaming: !canSendMessage, - onStopStreaming: () { - chatBloc.add(const ChatEvent.stopStream()); - }, - onSubmitted: (text, format, metadata) { - chatBloc.add( - ChatEvent.sendMessage( - message: text, - format: format, - metadata: metadata, - ), - ); - }, - onUpdateSelectedSources: (ids) { - chatBloc.add( - ChatEvent.updateSelectedSources( - selectedSourcesIds: ids, - ), - ); - }, - ); - }, - ), - ), - ); - }, - ); - } - void _onSelectMetadata( BuildContext context, ChatMessageRefSource metadata, @@ -406,3 +348,94 @@ class _ChatContentPage extends StatelessWidget { } } } + +class _Input extends StatelessWidget { + const _Input({ + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.isSelectingMessages, + builder: (context, isSelectingMessages) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + transitionBuilder: (child, animation) { + return SizeTransition( + sizeFactor: animation, + axisAlignment: -1, + child: child, + ); + }, + child: isSelectingMessages + ? const SizedBox.shrink() + : Padding( + padding: AIChatUILayout.safeAreaInsets(context), + child: BlocSelector( + selector: (state) { + return state.promptResponseState == + PromptResponseState.ready; + }, + builder: (context, canSendMessage) { + final chatBloc = context.read(); + + return UniversalPlatform.isDesktop + ? DesktopPromptInput( + isStreaming: !canSendMessage, + onStopStreaming: () { + chatBloc.add(const ChatEvent.stopStream()); + }, + onSubmitted: (text, format, metadata) { + chatBloc.add( + ChatEvent.sendMessage( + message: text, + format: format, + metadata: metadata, + ), + ); + }, + selectedSourcesNotifier: + chatBloc.selectedSourcesNotifier, + onUpdateSelectedSources: (ids) { + chatBloc.add( + ChatEvent.updateSelectedSources( + selectedSourcesIds: ids, + ), + ); + }, + ) + : MobileChatInput( + isStreaming: !canSendMessage, + onStopStreaming: () { + chatBloc.add(const ChatEvent.stopStream()); + }, + onSubmitted: (text, format, metadata) { + chatBloc.add( + ChatEvent.sendMessage( + message: text, + format: format, + metadata: metadata, + ), + ); + }, + selectedSourcesNotifier: + chatBloc.selectedSourcesNotifier, + onUpdateSelectedSources: (ids) { + chatBloc.add( + ChatEvent.updateSelectedSources( + selectedSourcesIds: ids, + ), + ); + }, + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart index 06694e3aa3..4d5cd82098 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart @@ -1,8 +1,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:extended_text_field/extended_text_field.dart'; @@ -10,21 +9,19 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../layout_define.dart'; - class MobileChatInput extends StatefulWidget { const MobileChatInput({ super.key, - required this.chatId, required this.isStreaming, required this.onStopStreaming, required this.onSubmitted, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, }); - final String chatId; final bool isStreaming; final void Function() onStopStreaming; + final ValueNotifier> selectedSourcesNotifier; final void Function(String, PredefinedFormat?, Map) onSubmitted; final void Function(List) onUpdateSelectedSources; @@ -38,11 +35,6 @@ class _MobileChatInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); - bool showPredefinedFormatSection = true; - PredefinedFormat predefinedFormat = const PredefinedFormat( - imageFormat: ImageFormat.text, - textFormat: TextFormat.bulletList, - ); late SendButtonState sendButtonState; @override @@ -106,53 +98,62 @@ class _MobileChatInputState extends State { borderRadius: const BorderRadius.vertical(top: Radius.circular(8.0)), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: - MobileAIPromptSizes.attachedFilesBarPadding.vertical + + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MobileAIPromptSizes + .attachedFilesBarPadding.vertical + MobileAIPromptSizes.attachedFilesPreviewHeight, - ), - child: PromptInputFile( - chatId: widget.chatId, - onDeleted: (file) => context - .read() - .add(AIPromptInputEvent.removeFile(file)), - ), - ), - if (showPredefinedFormatSection) - TextFieldTapRegion( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ChangeFormatBar( - predefinedFormat: predefinedFormat, - spacing: 8.0, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); - }, + ), + child: PromptInputFile( + onDeleted: (file) => context + .read() + .add(AIPromptInputEvent.removeFile(file)), ), ), - ) - else - const VSpace(8.0), - inputTextField(context), - TextFieldTapRegion( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const HSpace(8.0), - leadingButtons(context), - const Spacer(), - sendButton(), - const HSpace(12.0), - ], + if (state.showPredefinedFormats) + TextFieldTapRegion( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ChangeFormatBar( + predefinedFormat: state.predefinedFormat, + spacing: 8.0, + onSelectPredefinedFormat: (format) => + context.read().add( + AIPromptInputEvent.updatePredefinedFormat( + format, + ), + ), + ), + ), + ) + else + const VSpace(8.0), + inputTextField(context), + TextFieldTapRegion( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const HSpace(8.0), + leadingButtons( + context, + state.showPredefinedFormats, + ), + const Spacer(), + sendButton(), + const HSpace(12.0), + ], + ), + ), ), - ), - ), - ], + ], + ); + }, ), ), ), @@ -185,9 +186,13 @@ class _MobileChatInputState extends State { // get the attached files and mentioned pages final metadata = context.read().consumeMetadata(); + final bloc = context.read(); + final showPredefinedFormats = bloc.state.showPredefinedFormats; + final predefinedFormat = bloc.state.predefinedFormat; + widget.onSubmitted( trimmedText, - showPredefinedFormatSection ? predefinedFormat : null, + showPredefinedFormats ? predefinedFormat : null, metadata, ); } @@ -266,7 +271,7 @@ class _MobileChatInputState extends State { AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() }, - hintStyle: AIChatUILayout.inputHintTextStyle(context), + hintStyle: inputHintTextStyle(context), isCollapsed: true, isDense: true, ), @@ -289,7 +294,15 @@ class _MobileChatInputState extends State { ); } - Widget leadingButtons(BuildContext context) { + TextStyle? inputHintTextStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).isLightMode + ? const Color(0xFFBDC2C8) + : const Color(0xFF3C3E51), + ); + } + + Widget leadingButtons(BuildContext context, bool showPredefinedFormats) { return _LeadingActions( // onMention: () { // textController.text += '@'; @@ -300,13 +313,13 @@ class _MobileChatInputState extends State { // mentionPage(context); // }); // }, - showPredefinedFormatSection: showPredefinedFormatSection, - predefinedFormat: predefinedFormat, + showPredefinedFormats: showPredefinedFormats, onTogglePredefinedFormatSection: () { - setState(() { - showPredefinedFormatSection = !showPredefinedFormatSection; - }); + context + .read() + .add(AIPromptInputEvent.toggleShowPredefinedFormat()); }, + selectedSourcesNotifier: widget.selectedSourcesNotifier, onUpdateSelectedSources: widget.onUpdateSelectedSources, ); } @@ -322,15 +335,15 @@ class _MobileChatInputState extends State { class _LeadingActions extends StatelessWidget { const _LeadingActions({ - required this.showPredefinedFormatSection, - required this.predefinedFormat, + required this.showPredefinedFormats, required this.onTogglePredefinedFormatSection, + required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, }); - final bool showPredefinedFormatSection; - final PredefinedFormat predefinedFormat; + final bool showPredefinedFormats; final void Function() onTogglePredefinedFormatSection; + final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; @override @@ -342,10 +355,11 @@ class _LeadingActions extends StatelessWidget { separatorBuilder: () => const HSpace(4.0), children: [ PromptInputMobileSelectSourcesButton( + selectedSourcesNotifier: selectedSourcesNotifier, onUpdateSelectedSources: onUpdateSelectedSources, ), PromptInputMobileToggleFormatButton( - showFormatBar: showPredefinedFormatSection, + showFormatBar: showPredefinedFormats, onTap: onTogglePredefinedFormatSection, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart index eddf778e8a..611ff5d922 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/util/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -20,14 +19,6 @@ class AIChatUILayout { static EdgeInsets get messageMargin => UniversalPlatform.isMobile ? const EdgeInsets.symmetric(horizontal: 16) : EdgeInsets.zero; - - static TextStyle? inputHintTextStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).isLightMode - ? const Color(0xFFBDC2C8) - : const Color(0xFF3C3E51), - ); - } } class DesktopAIChatSizes { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart index f68453df75..5fa3b8f8a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index bb79a6b2d3..48a8459598 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -1,14 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:appflowy/ai/widgets/prompt_input/layout_define.dart'; -import 'package:appflowy/ai/widgets/prompt_input/predefined_format_buttons.dart'; -import 'package:appflowy/ai/widgets/prompt_input/select_sources_menu.dart'; +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; @@ -220,7 +217,7 @@ class RegenerateButton extends StatelessWidget { ? DesktopAIChatSizes.messageHoverActionBarIconRadius : DesktopAIChatSizes.messageActionBarIconRadius, icon: FlowySvg( - FlowySvgs.ai_undo_s, + FlowySvgs.ai_try_again_s, color: Theme.of(context).hintColor, size: const Size.square(16), ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index 802b8d6142..a938c9094f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -1,11 +1,11 @@ import 'dart:convert'; +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; @@ -22,7 +22,6 @@ import 'package:go_router/go_router.dart'; import 'package:universal_platform/universal_platform.dart'; import '../chat_avatar.dart'; -import '../../../../ai/widgets/prompt_input/mention_page_bottom_sheet.dart'; import '../layout_define.dart'; import 'ai_message_action_bar.dart'; import 'ai_change_format_bottom_sheet.dart'; @@ -377,7 +376,7 @@ class ChatAIMessagePopup extends StatelessWidget { onRegenerate?.call(); Navigator.of(context).pop(); }, - icon: FlowySvgs.ai_undo_s, + icon: FlowySvgs.ai_try_again_s, iconSize: const Size.square(20), text: LocaleKeys.chat_regenerate.tr(), ); @@ -470,7 +469,9 @@ class _WrapIsSelectingMessage extends StatelessWidget { if (isSelectingMessages) ChatSelectMessageIndicator(isSelected: isSelected) else - const ChatAIAvatar(), + SelectionContainer.disabled( + child: const ChatAIAvatar(), + ), const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), Expanded( child: IgnorePointer( diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index a54d2b8044..d6b3c87903 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/ai/widgets/loading_indicator.dart'; +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; @@ -84,14 +84,20 @@ class ChatAIMessageWidget extends StatelessWidget { loading: () => ChatAIMessageBubble( message: message, showActions: false, - child: AILoadingIndicator(text: loadingText), + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: AILoadingIndicator(text: loadingText), + ), ), ready: () { return state.text.isEmpty ? ChatAIMessageBubble( message: message, showActions: false, - child: AILoadingIndicator(text: loadingText), + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: AILoadingIndicator(text: loadingText), + ), ) : ChatAIMessageBubble( message: message, @@ -107,9 +113,11 @@ class ChatAIMessageWidget extends StatelessWidget { children: [ AIMarkdownText(markdown: state.text), if (state.sources.isNotEmpty) - AIMessageMetadata( - sources: state.sources, - onSelectedMetadata: onSelectedMetadata, + SelectionContainer.disabled( + child: AIMessageMetadata( + sources: state.sources, + onSelectedMetadata: onSelectedMetadata, + ), ), if (state.sources.isNotEmpty && !isLastMessage) const VSpace(8.0), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart index 8f897fc8a1..8bd115ad0f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart @@ -59,9 +59,11 @@ class ChatUserMessageBubble extends StatelessWidget { return BlocBuilder( builder: (context, state) { final member = state.members[message.author.id]; - return ChatUserAvatar( - iconUrl: member?.info.avatarUrl ?? "", - name: member?.info.name ?? "", + return SelectionContainer.disabled( + child: ChatUserAvatar( + iconUrl: member?.info.avatarUrl ?? "", + name: member?.info.name ?? "", + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index b789de3881..2094462d6d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ask_ai_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -163,7 +163,7 @@ extension on InsertOperation { Path currentPath = path; final List actions = []; for (final node in nodes) { - if (node.type == AskAIBlockKeys.type) { + if (node.type == AiWriterBlockKeys.type) { continue; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 2d6a52a58d..d5e44301ad 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -343,11 +343,7 @@ Map _buildBlockComponentBuilderMap( configuration, styleCustomizer, ), - AIWriterBlockKeys.type: _buildAIWriterBlockComponentBuilder( - context, - configuration, - ), - AskAIBlockKeys.type: _buildAskAIBlockComponentBuilder( + AiWriterBlockKeys.type: _buildAIWriterBlockComponentBuilder( context, configuration, ), @@ -836,13 +832,6 @@ AIWriterBlockComponentBuilder _buildAIWriterBlockComponentBuilder( return AIWriterBlockComponentBuilder(); } -AskAIBlockComponentBuilder _buildAskAIBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return AskAIBlockComponentBuilder(); -} - ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder( BuildContext context, BlockComponentConfiguration configuration, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 1425c2867c..9d1119ecf1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -80,7 +80,8 @@ class _AppFlowyEditorPageState extends State ]; final List toolbarItems = [ - askAIItem..isActive = onlyShowInTextType, + improveWritingItem..isActive = onlyShowInTextType, + aiWriterItem..isActive = onlyShowInTextType, paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, headingsToolbarItem ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 51e8cf0276..fd1d0c4c82 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -1,49 +1,53 @@ -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy/ai/service/error.dart'; +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; -import 'widgets/ai_limit_dialog.dart'; -import 'widgets/ai_writer_block_operations.dart'; -import 'widgets/ai_writer_block_widgets.dart'; -import 'widgets/discard_dialog.dart'; -import 'widgets/barrier_dialog.dart'; +import 'operations/ai_writer_cubit.dart'; +import 'operations/ai_writer_entities.dart'; +import 'operations/ai_writer_node_extension.dart'; +import 'suggestion_action_bar.dart'; -class AIWriterBlockKeys { - const AIWriterBlockKeys._(); +class AiWriterBlockKeys { + const AiWriterBlockKeys._(); static const String type = 'ai_writer'; - static const String prompt = 'prompt'; - static const String startSelection = 'start_selection'; - static const String generationCount = 'generation_count'; - static String getRewritePrompt(String previousOutput, String prompt) { - return 'I am not satisfied with your previous response ($previousOutput) to the query ($prompt). Please provide an alternative response.'; - } + static const String isInitialized = 'is_initialized'; + static const String selection = 'selection'; + static const String command = 'command'; + + /// Sample usage: + /// + /// `attributes: { + /// 'ai_writer_delta_suggestion': 'original' + /// }` + static const String suggestion = 'ai_writer_delta_suggestion'; + static const String suggestionOriginal = 'original'; + static const String suggestionReplacement = 'replacement'; } Node aiWriterNode({ - String prompt = '', - required Selection start, + required Selection? selection, + required AiWriterCommand command, }) { return Node( - type: AIWriterBlockKeys.type, + type: AiWriterBlockKeys.type, attributes: { - AIWriterBlockKeys.prompt: prompt, - AIWriterBlockKeys.startSelection: start.toJson(), - AIWriterBlockKeys.generationCount: 0, + AiWriterBlockKeys.isInitialized: false, + AiWriterBlockKeys.selection: selection?.toJson(), + AiWriterBlockKeys.command: command.index, }, ); } @@ -54,7 +58,7 @@ class AIWriterBlockComponentBuilder extends BlockComponentBuilder { @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { final node = blockComponentContext.node; - return AIWriterBlockComponent( + return AiWriterBlockComponent( key: node.key, node: node, showActions: showActions(node), @@ -68,12 +72,13 @@ class AIWriterBlockComponentBuilder extends BlockComponentBuilder { @override BlockComponentValidate get validate => (node) => node.children.isEmpty && - node.attributes[AIWriterBlockKeys.prompt] is String && - node.attributes[AIWriterBlockKeys.startSelection] is Map; + node.attributes[AiWriterBlockKeys.isInitialized] is bool && + node.attributes[AiWriterBlockKeys.selection] is Map? && + node.attributes[AiWriterBlockKeys.command] is int; } -class AIWriterBlockComponent extends BlockComponentStatefulWidget { - const AIWriterBlockComponent({ +class AiWriterBlockComponent extends BlockComponentStatefulWidget { + const AiWriterBlockComponent({ super.key, required super.node, super.showActions, @@ -82,51 +87,42 @@ class AIWriterBlockComponent extends BlockComponentStatefulWidget { }); @override - State createState() => _AIWriterBlockComponentState(); + State createState() => _AIWriterBlockComponentState(); } -class _AIWriterBlockComponentState extends State { - final controller = TextEditingController(); +class _AIWriterBlockComponentState extends State { + final key = GlobalKey(); + final textController = TextEditingController(); final textFieldFocusNode = FocusNode(); + final overlayController = OverlayPortalController(); + final layerLink = LayerLink(); late final editorState = context.read(); - late final SelectionGestureInterceptor interceptor; - late final aiWriterOperations = AIWriterBlockOperations( + late final aiWriterCubit = AiWriterCubit( + documentId: context.read().documentId, editorState: editorState, - aiWriterNode: widget.node, + getAiWriterNode: () => widget.node, + initialCommand: widget.node.aiWriterCommand, ); - String get prompt => widget.node.attributes[AIWriterBlockKeys.prompt]; - int get generationCount => - widget.node.attributes[AIWriterBlockKeys.generationCount] ?? 0; - Selection? get startSelection { - final selection = widget.node.attributes[AIWriterBlockKeys.startSelection]; - if (selection != null) { - return Selection.fromJson(selection); - } - return null; - } - - bool isGenerating = false; - @override void initState() { super.initState(); - _subscribeSelectionGesture(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - editorState.selection = null; + WidgetsBinding.instance.addPostFrameCallback((_) { + overlayController.show(); textFieldFocusNode.requestFocus(); + if (!widget.node.isAiWriterInitialized) { + aiWriterCubit.init(); + } }); } @override void dispose() { - _onExit(); - _unsubscribeSelectionGesture(); - controller.dispose(); + textController.dispose(); textFieldFocusNode.dispose(); - + aiWriterCubit.close(); super.dispose(); } @@ -136,277 +132,479 @@ class _AIWriterBlockComponentState extends State { return const SizedBox.shrink(); } - final child = Focus( - onKeyEvent: (node, event) { - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - if (event.logicalKey == LogicalKeyboardKey.enter) { - if (!isGenerating) { - _onGenerate(); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: Card( - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: aiWriterCubit, ), - color: Theme.of(context).colorScheme.surface, - child: Container( - margin: const EdgeInsets.all(10), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const AIWriterBlockHeader(), - const Space(0, 10), - if (prompt.isEmpty && generationCount < 1) ...[ - _buildInputWidget(context), - const Space(0, 10), - AIWriterBlockInputField( - onGenerate: _onGenerate, - onExit: _onExit, - ), - ] else ...[ - AIWriterBlockFooter( - onKeep: _onExit, - onRewrite: _onRewrite, - onDiscard: _onDiscard, - ), - ], - ], + BlocProvider( + create: (_) => AIPromptInputBloc( + predefinedFormat: null, ), ), + ], + child: LayoutBuilder( + builder: (context, constraints) { + return BlocListener( + listener: (context, state) { + if (state is SingleShotAiWriterState) { + showConfirmDialog( + context: context, + title: state.title, + description: state.description, + onConfirm: state.onDismiss, + ); + } + }, + child: OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return Stack( + children: [ + BlocBuilder( + builder: (context, state) { + final hitTestBehavior = state is GeneratingAiWriterState + ? HitTestBehavior.opaque + : HitTestBehavior.translucent; + return GestureDetector( + behavior: hitTestBehavior, + onTap: () => onTapOutside(), + ); + }, + ), + CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + child: Container( + padding: const EdgeInsets.only( + left: 40.0, + bottom: 16.0, + ), + width: constraints.maxWidth, + child: OverlayContent( + node: widget.node, + ), + ), + ), + ], + ); + }, + child: CompositedTransformTarget( + link: layerLink, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + key: key, + width: double.infinity, + ); + }, + ), + ), + ), + ); + }, ), ); - - return Padding( - padding: const EdgeInsets.only(left: 40), - child: child, - ); } - Widget _buildInputWidget(BuildContext context) { - return FlowyTextField( - hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(), - controller: controller, - maxLines: 5, - focusNode: textFieldFocusNode, - autoFocus: false, - hintTextConstraints: const BoxConstraints(), - ); - } - - Future _onExit() async { - await aiWriterOperations.removeAIWriterNode(widget.node); - } - - Future _onGenerate() async { - if (isGenerating) { - return; - } - - isGenerating = true; - - await aiWriterOperations.updatePromptText(controller.text); - - if (!_isAIWriterEnabled) { - Log.error('AI Writer is not enabled'); - return; - } - - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - - BarrierDialog? barrierDialog; - - final aiRepository = AppFlowyAIService(); - final objectId = - editorState.document.root.context?.read().documentId ?? - ""; - - await aiRepository.streamCompletion( - objectId: objectId, - text: controller.text, - completionType: CompletionTypePB.ContinueWriting, - onStart: () async { - if (mounted) { - barrierDialog = BarrierDialog(context); - barrierDialog?.show(); - await aiWriterOperations.ensurePreviousNodeIsEmptyParagraphNode(); - markdownTextRobot.start(); - } - }, - onProcess: (text) async { - await markdownTextRobot.appendMarkdownText(text); - }, - onEnd: () async { - barrierDialog?.dismiss(); - await markdownTextRobot.stop(); - editorState.service.keyboardService?.enable(); - }, - onError: (error) async { - barrierDialog?.dismiss(); - _showAIWriterError(error); - }, - ); - - await aiWriterOperations.updateGenerationCount(generationCount + 1); - - isGenerating = false; - } - - Future _onDiscard() async { - await aiWriterOperations.discardCurrentResponse( - aiWriterNode: widget.node, - selection: startSelection, - ); - return _onExit(); - } - - Future _onRewrite() async { - if (isGenerating) { - return; - } - - isGenerating = true; - - final previousOutput = _getPreviousOutput(); - if (previousOutput == null) { - return; - } - - // discard the current response - await aiWriterOperations.discardCurrentResponse( - aiWriterNode: widget.node, - selection: startSelection, - ); - - if (!_isAIWriterEnabled) { - return; - } - - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - final aiService = AppFlowyAIService(); - final objectId = - editorState.document.root.context?.read().documentId ?? - ""; - await aiService.streamCompletion( - objectId: objectId, - text: AIWriterBlockKeys.getRewritePrompt(previousOutput, prompt), - completionType: CompletionTypePB.ContinueWriting, - onStart: () async { - await aiWriterOperations.ensurePreviousNodeIsEmptyParagraphNode(); - - markdownTextRobot.start(); - }, - onProcess: (text) async { - await markdownTextRobot.appendMarkdownText(text); - }, - onEnd: () async { - await markdownTextRobot.stop(); - }, - onError: (error) { - _showAIWriterError(error); - }, - ); - - await aiWriterOperations.updateGenerationCount(generationCount + 1); - - isGenerating = false; - } - - String? _getPreviousOutput() { - final startSelection = this.startSelection; - if (startSelection != null) { - final end = widget.node.previous?.path; - - if (end != null) { - final result = editorState - .getNodesInSelection( - startSelection.copyWith(end: Position(path: end)), - ) - .fold( - '', - (previousValue, element) { - final delta = element.delta; - if (delta != null) { - return "$previousValue\n${delta.toPlainText()}"; - } else { - return previousValue; - } - }, - ); - return result.trim(); - } - } - return null; - } - - void _subscribeSelectionGesture() { - interceptor = SelectionGestureInterceptor( - key: AIWriterBlockKeys.type, - canTap: (details) { - if (!context.isOffsetInside(details.globalPosition)) { - if (prompt.isNotEmpty || controller.text.isNotEmpty) { - // show dialog - showDialog( - context: context, - builder: (_) => DiscardDialog( - onConfirm: _onDiscard, - onCancel: () {}, - ), - ); - } else if (controller.text.isEmpty) { - _onExit(); - } - } - editorState.service.keyboardService?.disable(); - return false; - }, - ); - editorState.service.selectionService.registerGestureInterceptor( - interceptor, - ); - } - - void _unsubscribeSelectionGesture() { - editorState.service.selectionService.unregisterGestureInterceptor( - AIWriterBlockKeys.type, - ); - } - - void _showAIWriterError(AIError error) { - if (mounted) { - if (error.isLimitExceeded) { - showAILimitDialog(context, error.message); - } else { - showToastNotification( - context, - message: error.message, - type: ToastificationType.error, - ); - } - } - } - - bool get _isAIWriterEnabled { - final userProfile = context.read().state.userProfilePB; - final isAIWriterEnabled = userProfile != null; - - if (!isAIWriterEnabled) { - showToastNotification( - context, - message: LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(), - type: ToastificationType.error, + void onTapOutside() { + if (aiWriterCubit.hasUnusedResponse()) { + showConfirmDialog( + context: context, + title: LocaleKeys.button_discard.tr(), + description: LocaleKeys.document_plugins_discardResponse.tr(), + confirmLabel: LocaleKeys.button_discard.tr(), + style: ConfirmPopupStyle.cancelAndOk, + onConfirm: () => aiWriterCubit + ..stopStream() + ..exit(), + onCancel: () {}, ); + } else { + aiWriterCubit + ..stopStream() + ..exit(); } - - return isAIWriterEnabled; + } +} + +class OverlayContent extends StatelessWidget { + const OverlayContent({ + super.key, + required this.node, + }); + + final Node node; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final selection = node.aiWriterSelection; + final showSuggestionPopup = + state is ReadyAiWriterState && !state.isFirstRun; + final showActionPopup = state is ReadyAiWriterState && state.isFirstRun; + final markdownText = switch (state) { + final ReadyAiWriterState ready => ready.markdownText, + final GeneratingAiWriterState generating => generating.markdownText, + _ => '', + }; + final hasSelection = selection != null && !selection.isCollapsed; + + final isLightMode = Theme.of(context).isLightMode; + final darkBorderColor = + isLightMode ? Color(0x1F1F2329) : Color(0xFF505469); + final lightBorderColor = + Theme.of(context).brightness == Brightness.light + ? ColorSchemeConstants.lightBorderColor + : ColorSchemeConstants.darkBorderColor; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showSuggestionPopup && + state.command != AiWriterCommand.explain) ...[ + Container( + padding: EdgeInsets.all(4.0), + decoration: _getModalDecoration( + context, + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderColor: darkBorderColor, + ), + child: SuggestionActionBar( + actions: _getSuggestedActions( + currentCommand: state.command, + hasSelection: hasSelection, + ), + onTap: (action) { + context.read().runResponseAction(action); + }, + ), + ), + const VSpace(4.0 + 1.0), + ], + DecoratedBox( + decoration: _getModalDecoration( + context, + color: null, + borderColor: darkBorderColor, + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ), + child: Column( + children: [ + if (markdownText.isNotEmpty) ...[ + DecoratedBox( + decoration: _getHelperChildDecoration(context), + child: Container( + constraints: BoxConstraints(maxHeight: 140), + width: double.infinity, + child: SingleChildScrollView( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 24.0, + padding: EdgeInsets.symmetric(horizontal: 6.0), + alignment: AlignmentDirectional.centerStart, + child: FlowyText( + state.command.i18n, + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF666D76), + ), + ), + const VSpace(4.0), + Padding( + padding: EdgeInsets.symmetric(horizontal: 6.0), + child: AIMarkdownText( + markdown: markdownText, + ), + ), + if (showSuggestionPopup) ...[ + const VSpace(4.0), + SuggestionActionBar( + actions: _getSuggestedActions( + currentCommand: state.command, + hasSelection: hasSelection, + ), + onTap: (action) { + context + .read() + .runResponseAction(action); + }, + ), + ], + ], + ), + ), + ), + ), + Divider( + height: 1.0, + ), + ], + DecoratedBox( + decoration: markdownText.isNotEmpty + ? _getInputChildDecoration(context) + : _getSingleChildDeocoration(context), + child: MainContentArea(), + ), + ], + ), + ), + if (showActionPopup) ...[ + const VSpace(4.0 + 1.0), + Container( + padding: EdgeInsets.all(8.0), + constraints: BoxConstraints(minWidth: 240.0), + decoration: _getModalDecoration( + context, + color: Theme.of(context).colorScheme.surface, + borderColor: lightBorderColor, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + child: IntrinsicWidth( + child: SeparatedColumn( + separatorBuilder: () => const VSpace(4.0), + crossAxisAlignment: CrossAxisAlignment.start, + children: _getCommands( + hasSelection: hasSelection, + ), + ), + ), + ), + ], + ], + ); + }, + ); + } + + Widget _bottomButton(AiWriterCommand command) { + return Builder( + builder: (context) { + return SizedBox( + height: 30.0, + child: FlowyButton( + leftIcon: FlowySvg( + command.icon, + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, + ), + margin: const EdgeInsets.all(6.0), + text: FlowyText( + command.i18n, + figmaLineHeight: 20, + ), + onTap: () { + final aiInputBloc = context.read(); + final showPredefinedFormats = + aiInputBloc.state.showPredefinedFormats; + final predefinedFormat = aiInputBloc.state.predefinedFormat; + + context.read().runCommand( + command, + showPredefinedFormats ? predefinedFormat : null, + ); + }, + ), + ); + }, + ); + } + + BoxDecoration _getModalDecoration( + BuildContext context, { + required Color? color, + required Color borderColor, + required BorderRadius borderRadius, + }) { + return BoxDecoration( + color: color, + border: Border.all( + color: borderColor, + strokeAlign: BorderSide.strokeAlignOutside, + ), + borderRadius: borderRadius, + boxShadow: const [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 20, + color: Color(0x1A1F2329), + ), + ], + ); + } + + BoxDecoration _getSingleChildDeocoration(BuildContext context) { + return BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.all(Radius.circular(12.0)), + ); + } + + BoxDecoration _getHelperChildDecoration(BuildContext context) { + return BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)), + ); + } + + BoxDecoration _getInputChildDecoration(BuildContext context) { + return BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12.0)), + ); + } + + List _getCommands({required bool hasSelection}) { + if (hasSelection) { + return [ + _bottomButton(AiWriterCommand.improveWriting), + _bottomButton(AiWriterCommand.fixSpellingAndGrammar), + _bottomButton(AiWriterCommand.explain), + const Divider(height: 1.0, thickness: 1.0), + _bottomButton(AiWriterCommand.makeLonger), + _bottomButton(AiWriterCommand.makeShorter), + ]; + } else { + return [ + _bottomButton(AiWriterCommand.continueWriting), + ]; + } + } + + List _getSuggestedActions({ + required AiWriterCommand currentCommand, + required bool hasSelection, + }) { + if (hasSelection) { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + AiWriterCommand.fixSpellingAndGrammar || + AiWriterCommand.improveWriting || + AiWriterCommand.makeShorter || + AiWriterCommand.makeLonger => + [ + SuggestionAction.accept, + SuggestionAction.discard, + SuggestionAction.insertBelow, + SuggestionAction.rewrite, + ], + }; + } else { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + _ => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + }; + } + } +} + +class MainContentArea extends StatelessWidget { + const MainContentArea({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final cubit = context.read(); + + if (state is ReadyAiWriterState) { + return DesktopPromptInput( + isStreaming: false, + hideDecoration: true, + onSubmitted: (message, format, _) => cubit.submit(message, format), + onStopStreaming: () => cubit.stopStream(), + selectedSourcesNotifier: cubit.selectedSourcesNotifier, + onUpdateSelectedSources: (sources) { + cubit.selectedSourcesNotifier.value = [ + ...sources, + ]; + }, + ); + } + if (state is GeneratingAiWriterState) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const HSpace(6.0), + Expanded( + child: AILoadingIndicator( + text: state.command == AiWriterCommand.explain + ? LocaleKeys.ai_analyzing.tr() + : LocaleKeys.ai_editing.tr(), + ), + ), + const HSpace(8.0), + PromptInputSendButton( + state: SendButtonState.streaming, + onSendPressed: () {}, + onStopStreaming: () => cubit.stopStream(), + ), + ], + ), + ); + } + if (state is ErrorAiWriterState) { + return Padding( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.toast_error_filled_s, + blendMode: null, + ), + const HSpace(8.0), + Expanded( + child: FlowyText( + state.error.message, + maxLines: null, + ), + ), + const HSpace(8.0), + FlowyIconButton( + width: 32, + hoverColor: Colors.transparent, + icon: FlowySvg( + FlowySvgs.toast_close_s, + size: Size.square(20), + ), + onPressed: () => cubit.exit(), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart new file mode 100644 index 0000000000..96fb8289d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -0,0 +1,241 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'ai_writer_block_component.dart'; +import 'operations/ai_writer_entities.dart'; + +const _improveWritingToolbarItemId = 'appflowy.editor.ai_improve_writing'; +const _aiWriterToolbarItemId = 'appflowy.editor.ai_writer'; + +final ToolbarItem improveWritingItem = ToolbarItem( + id: _improveWritingToolbarItemId, + group: 0, + isActive: onlyShowInSingleSelectionAndTextType, + builder: (context, editorState, _, __, tooltipBuilder) => + ImproveWritingButton( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + ), +); + +final ToolbarItem aiWriterItem = ToolbarItem( + id: _aiWriterToolbarItemId, + group: 0, + isActive: onlyShowInSingleSelectionAndTextType, + builder: (context, editorState, _, __, tooltipBuilder) => + AiWriterToolbarActionList( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + ), +); + +class AiWriterToolbarActionList extends StatefulWidget { + const AiWriterToolbarActionList({ + super.key, + required this.editorState, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + + @override + State createState() => + _AiWriterToolbarActionListState(); +} + +class _AiWriterToolbarActionListState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(-8.0, 2.0), + margin: const EdgeInsets.all(8.0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(), + ); + } + + Widget buildPopoverContent() { + return SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(4.0), + children: [ + actionWrapper(AiWriterCommand.improveWriting), + actionWrapper(AiWriterCommand.userQuestion), + actionWrapper(AiWriterCommand.fixSpellingAndGrammar), + // actionWrapper(AiWriterCommand.summarize), + actionWrapper(AiWriterCommand.explain), + divider(), + actionWrapper(AiWriterCommand.makeLonger), + actionWrapper(AiWriterCommand.makeShorter), + ], + ); + } + + Widget actionWrapper(AiWriterCommand command) { + return SizedBox( + height: 30.0, + child: FlowyButton( + leftIcon: FlowySvg( + command.icon, + size: const Size.square(16), + ), + text: FlowyText( + command.i18n, + figmaLineHeight: 20, + ), + onTap: () { + popoverController.close(); + _insertAiNode(widget.editorState, command); + }, + ), + ); + } + + Widget divider() { + return const Divider( + thickness: 1.0, + height: 1.0, + ); + } + + Widget buildChild() { + final child = FlowyIconButton( + hoverColor: Colors.transparent, + width: 36, + height: 24, + icon: Row( + children: [ + const FlowySvg( + FlowySvgs.ai_sparks_s, + size: Size.square(16.0), + color: Color(0xFFD08EED), + ), + const FlowySvg( + FlowySvgs.ai_source_drop_down_s, + size: Size.square(12), + color: Color(0xFF8F959E), + ), + ], + ), + iconPadding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + onPressed: () { + if (_isAIEnabled(widget.editorState)) { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } else { + showToastNotification( + context, + message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), + ); + } + }, + ); + + return widget.tooltipBuilder?.call( + context, + _aiWriterToolbarItemId, + _isAIEnabled(widget.editorState) + ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr() + : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), + child, + ) ?? + child; + } +} + +class ImproveWritingButton extends StatelessWidget { + const ImproveWritingButton({ + super.key, + required this.editorState, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + + @override + Widget build(BuildContext context) { + final child = FlowyIconButton( + hoverColor: Colors.transparent, + width: 24, + icon: const FlowySvg( + FlowySvgs.ai_improve_writing_s, + size: Size.square(16.0), + color: Color(0xFFD08EED), + ), + iconPadding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + onPressed: () { + if (_isAIEnabled(editorState)) { + keepEditorFocusNotifier.increase(); + } else { + showToastNotification( + context, + message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), + ); + } + }, + ); + + return tooltipBuilder?.call( + context, + _aiWriterToolbarItemId, + _isAIEnabled(editorState) + ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr() + : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), + child, + ) ?? + child; + } +} + +void _insertAiNode(EditorState editorState, AiWriterCommand command) async { + final selection = editorState.selection?.normalized; + if (selection == null) { + return; + } + + final transaction = editorState.transaction + ..insertNode( + selection.end.path.next, + aiWriterNode( + selection: selection, + command: command, + ), + ) + ..afterSelection = selection + ..selectionExtraInfo = {selectionExtraInfoDisableToolbar: true}; + + await editorState.apply( + transaction, + options: const ApplyOptions( + recordUndo: false, + inMemoryUpdate: true, + ), + ); +} + +bool _isAIEnabled(EditorState editorState) { + final documentContext = editorState.document.root.context; + return documentContext == null || + !documentContext.read().isLocalMode; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_block_component.dart deleted file mode 100644 index df3724f90e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_block_component.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy/ai/service/error.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/ai/service/ai_client.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_limit_dialog.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_block_widgets.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class AskAIBlockKeys { - const AskAIBlockKeys._(); - - static const type = 'ask_ai'; - - /// The instruction of the smart edit. - /// - /// It is a [AskAIAction] value. - static const action = 'action'; - - /// The input of the smart edit. - /// - /// The content is a string that using '\n\n' as separator. - static const content = 'content'; -} - -Node askAINode({ - required AskAIAction action, - required String content, -}) { - return Node( - type: AskAIBlockKeys.type, - attributes: { - AskAIBlockKeys.action: action.index, - AskAIBlockKeys.content: content, - }, - ); -} - -class AskAIBlockComponentBuilder extends BlockComponentBuilder { - AskAIBlockComponentBuilder(); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return AskAIBlockComponentWidget( - key: node.key, - node: node, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => - node.attributes[AskAIBlockKeys.action] is int && - node.attributes[AskAIBlockKeys.content] is String; -} - -class AskAIBlockComponentWidget extends BlockComponentStatefulWidget { - const AskAIBlockComponentWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => - _AskAIBlockComponentWidgetState(); -} - -class _AskAIBlockComponentWidgetState extends State { - final popoverController = PopoverController(); - - late final editorState = context.read(); - late final action = - AskAIAction.values[widget.node.attributes[AskAIBlockKeys.action] as int]; - late AskAIActionBloc askAIBloc; - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - popoverController.show(); - }); - - final objectId = - editorState.document.root.context?.read().documentId ?? - ""; - - askAIBloc = AskAIActionBloc( - objectId: objectId, - node: widget.node, - editorState: editorState, - action: action, - )..add(AskAIEvent.initial(getIt.getAsync())); - } - - @override - void dispose() { - askAIBloc.close(); - - super.dispose(); - } - - @override - void reassemble() { - super.reassemble(); - - _removeNode(); - } - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isMobile) { - return const SizedBox.shrink(); - } - - final width = _getEditorWidth(); - - return BlocProvider.value( - value: askAIBloc, - child: BlocListener( - listener: _onListen, - child: AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - offset: const Offset(40, 0), // align the editor block - windowPadding: EdgeInsets.zero, - constraints: BoxConstraints(maxWidth: width), - canClose: () async { - final completer = Completer(); - final state = askAIBloc.state; - if (state.result.isEmpty) { - completer.complete(true); - } else { - await showCancelAndConfirmDialog( - context: context, - title: LocaleKeys.document_plugins_discardResponse.tr(), - description: '', - confirmLabel: LocaleKeys.button_discard.tr(), - onConfirm: () => completer.complete(true), - onCancel: () => completer.complete(false), - ); - } - return completer.future; - }, - onClose: _removeNode, - popupBuilder: (BuildContext popoverContext) { - return BlocProvider.value( - // request the result when opening the popover - value: askAIBloc..add(const AskAIEvent.started()), - child: const AskAIInputContent(), - ); - }, - child: const SizedBox( - width: double.infinity, - ), - ), - ), - ); - } - - double _getEditorWidth() { - var width = double.infinity; - try { - final editorSize = editorState.renderBox?.size; - final editorWidth = - editorSize?.width.clamp(0, editorState.editorStyle.maxWidth ?? width); - final padding = editorState.editorStyle.padding; - if (editorWidth != null) { - width = editorWidth - padding.left - padding.right; - } - } catch (_) {} - return width; - } - - void _removeNode() { - final transaction = editorState.transaction..deleteNode(widget.node); - editorState.apply(transaction); - } - - void _onListen(BuildContext context, AskAIState state) { - final error = state.requestError; - if (error != null) { - if (error.isLimitExceeded) { - showAILimitDialog(context, error.message); - } else { - showToastNotification( - context, - message: error.message, - type: ToastificationType.error, - ); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart deleted file mode 100644 index 9b413e1270..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ask_ai_toolbar_item.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'ask_ai_block_component.dart'; -import 'widgets/ask_ai_action.dart'; - -const _kAskAIToolbarItemId = 'appflowy.editor.ask_ai'; - -final ToolbarItem askAIItem = ToolbarItem( - id: _kAskAIToolbarItemId, - group: 0, - isActive: onlyShowInSingleSelectionAndTextType, - builder: (context, editorState, _, __, tooltipBuilder) => AskAIActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ), -); - -class AskAIActionList extends StatefulWidget { - const AskAIActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - - @override - State createState() => _AskAIActionListState(); -} - -class _AskAIActionListState extends State { - late bool isAIEnabled; - - EditorState get editorState => widget.editorState; - - @override - void initState() { - super.initState(); - _updateIsAIEnabled(); - } - - @override - Widget build(BuildContext context) { - return PopoverActionList( - offset: const Offset(-5, 5), - direction: PopoverDirection.bottomWithLeftAligned, - actions: AskAIAction.values - .map((action) => AskAIActionWrapper(action)) - .toList(), - onClosed: () => keepEditorFocusNotifier.decrease(), - buildChild: (controller) { - keepEditorFocusNotifier.increase(); - final child = FlowyButton( - text: FlowyText.regular( - LocaleKeys.document_plugins_smartEdit.tr(), - fontSize: 13.0, - figmaLineHeight: 16.0, - color: Colors.white, - ), - hoverColor: Colors.transparent, - useIntrinsicWidth: true, - leftIcon: const FlowySvg( - FlowySvgs.toolbar_item_ai_s, - size: Size.square(16.0), - color: Colors.white, - ), - onTap: () { - if (isAIEnabled) { - keepEditorFocusNotifier.increase(); - controller.show(); - } else { - showToastNotification( - context, - message: - LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - ); - } - }, - ); - - if (widget.tooltipBuilder != null) { - return widget.tooltipBuilder!( - context, - _kAskAIToolbarItemId, - isAIEnabled - ? LocaleKeys.document_plugins_smartEdit.tr() - : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - child, - ); - } - - return child; - }, - onSelected: (action, controller) { - controller.close(); - _insertAskAINode(action); - }, - ); - } - - Future _insertAskAINode( - AskAIActionWrapper actionWrapper, - ) async { - final selection = editorState.selection?.normalized; - if (selection == null) { - return; - } - - final markdown = await editorState.getMarkdownInSelection(selection); - - final transaction = editorState.transaction; - transaction.insertNode( - selection.normalized.end.path.next, - askAINode( - action: actionWrapper.inner, - content: markdown, - ), - ); - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - inMemoryUpdate: true, - ), - withUpdateSelection: false, - ); - } - - void _updateIsAIEnabled() { - final documentContext = widget.editorState.document.root.context; - isAIEnabled = documentContext == null || - !documentContext.read().isLocalMode; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart new file mode 100644 index 0000000000..c82b39e241 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart @@ -0,0 +1,209 @@ +import 'package:appflowy/shared/markdown_to_document.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +import '../ai_writer_block_component.dart'; +import 'ai_writer_entities.dart'; + +Future removeAiWriterNode(EditorState editorState, Node node) async { + final transaction = editorState.transaction..deleteNode(node); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, + ); +} + +void formatSelection( + EditorState editorState, + Selection selection, + Transaction transaction, + ApplySuggestionFormatType formatType, +) { + final nodes = editorState.getNodesInSelection(selection).toList(); + if (nodes.isEmpty) { + return; + } + + if (nodes.length == 1) { + final node = nodes.removeAt(0); + if (node.delta != null) { + final delta = Delta() + ..retain(selection.start.offset) + ..retain( + selection.length, + attributes: formatType.attributes, + ); + transaction.addDeltaToComposeMap(node, delta); + } + } else { + final firstNode = nodes.removeAt(0); + final lastNode = nodes.removeLast(); + + if (firstNode.delta != null) { + final text = firstNode.delta!.toPlainText(); + final remainderLength = text.length - selection.start.offset; + final delta = Delta() + ..retain(selection.start.offset) + ..retain(remainderLength, attributes: formatType.attributes); + transaction.addDeltaToComposeMap(firstNode, delta); + } + + if (lastNode.delta != null) { + final delta = Delta() + ..retain(selection.end.offset, attributes: formatType.attributes); + transaction.addDeltaToComposeMap(lastNode, delta); + } + + for (final node in nodes) { + if (node.delta == null) { + continue; + } + final length = node.delta!.length; + if (length != 0) { + final delta = Delta() + ..retain(length, attributes: formatType.attributes); + transaction.addDeltaToComposeMap(node, delta); + } + } + } + + transaction.compose(); +} + +Position ensurePreviousNodeIsEmptyParagraph( + EditorState editorState, + Node aiWriterNode, + Transaction transaction, +) { + final previous = aiWriterNode.previous; + final needsEmptyParagraphNode = previous == null || + previous.type != ParagraphBlockKeys.type || + (previous.delta?.toPlainText().isNotEmpty ?? false); + + final Position position; + if (needsEmptyParagraphNode) { + position = Position(path: aiWriterNode.path); + transaction.insertNode(aiWriterNode.path, paragraphNode()); + } else { + position = Position(path: previous.path); + } + + transaction.updateNode(aiWriterNode, { + AiWriterBlockKeys.isInitialized: true, + }); + + return position; +} + +extension SaveAIResponseExtension on EditorState { + Future insertBelow({ + required Node node, + required String markdownText, + }) async { + final selection = this.selection?.normalized; + if (selection == null) { + return; + } + + final nodes = customMarkdownToDocument( + markdownText, + tableWidth: 250.0, + ).root.children.map((e) => e.deepCopy()).toList(); + if (nodes.isEmpty) { + return; + } + + final insertedPath = selection.end.path.next; + final lastDeltaLength = nodes.lastOrNull?.delta?.length ?? 0; + + final transaction = this.transaction + ..insertNodes(insertedPath, nodes) + ..afterSelection = Selection( + start: Position(path: insertedPath), + end: Position( + path: insertedPath.nextNPath(nodes.length - 1), + offset: lastDeltaLength, + ), + ); + + await apply(transaction); + } + + Future replace({ + required Selection selection, + required String text, + }) async { + final trimmedText = text.trim(); + if (trimmedText.isEmpty) { + return; + } + await switch (kdefaultReplacementType) { + AskAIReplacementType.markdown => + _replaceWithMarkdown(selection, trimmedText), + AskAIReplacementType.plainText => + _replaceWithPlainText(selection, trimmedText), + }; + } + + Future _replaceWithMarkdown( + Selection selection, + String markdownText, + ) async { + final nodes = customMarkdownToDocument(markdownText) + .root + .children + .map((e) => e.deepCopy()) + .toList(); + if (nodes.isEmpty) { + return; + } + + final nodesInSelection = getNodesInSelection(selection); + final newSelection = Selection( + start: selection.start, + end: Position( + path: selection.start.path.nextNPath(nodes.length - 1), + offset: nodes.lastOrNull?.delta?.length ?? 0, + ), + ); + + final transaction = this.transaction + ..insertNodes(selection.start.path, nodes) + ..deleteNodes(nodesInSelection) + ..afterSelection = newSelection; + await apply(transaction); + } + + Future _replaceWithPlainText( + Selection selection, + String plainText, + ) async { + final nodes = getNodesInSelection(selection); + if (nodes.isEmpty || nodes.any((element) => element.delta == null)) { + return; + } + + final replaceTexts = plainText.split('\n') + ..removeWhere((element) => element.isEmpty); + final transaction = this.transaction + ..replaceTexts( + nodes, + selection, + replaceTexts, + ); + await apply(transaction); + + int endOffset = replaceTexts.last.length; + if (replaceTexts.length == 1) { + endOffset += selection.start.offset; + } + final end = Position( + path: [selection.start.path.first + replaceTexts.length - 1], + offset: endOffset, + ); + this.selection = Selection( + start: selection.start, + end: end, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart new file mode 100644 index 0000000000..e1c6efb337 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -0,0 +1,527 @@ +import 'dart:async'; + +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; + +import '../../base/markdown_text_robot.dart'; +import 'ai_writer_block_operations.dart'; +import 'ai_writer_entities.dart'; +import 'ai_writer_node_extension.dart'; + +class AiWriterCubit extends Cubit { + AiWriterCubit({ + required this.documentId, + required this.editorState, + required this.getAiWriterNode, + required this.initialCommand, + AppFlowyAIService? aiService, + }) : _aiService = aiService ?? AppFlowyAIService(), + _textRobot = MarkdownTextRobot(editorState: editorState), + selectedSourcesNotifier = ValueNotifier([documentId]), + super( + ReadyAiWriterState( + initialCommand, + isFirstRun: true, + ), + ); + + final String documentId; + final EditorState editorState; + final Node Function() getAiWriterNode; + final AiWriterCommand initialCommand; + final AppFlowyAIService _aiService; + final MarkdownTextRobot _textRobot; + + final ValueNotifier> selectedSourcesNotifier; + (String, PredefinedFormat?)? _previousPrompt; + + @override + Future close() async { + selectedSourcesNotifier.dispose(); + await super.close(); + } + + void init() { + runCommand(initialCommand, null, isImmediateRun: true); + } + + void submit( + String prompt, + PredefinedFormat? format, + ) async { + final command = AiWriterCommand.userQuestion; + final node = getAiWriterNode(); + _previousPrompt = (prompt, format); + + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: prompt, + format: format, + sourceIds: selectedSourcesNotifier.value, + completionType: command.toCompletionType(), + onStart: () async { + final transaction = editorState.transaction; + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); + _textRobot.start(position: position); + }, + onProcess: (text) async { + await _textRobot.appendMarkdownText( + text, + attributes: ApplySuggestionFormatType.replace.attributes, + ); + }, + onEnd: () async { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit(ReadyAiWriterState(command, isFirstRun: false)); + }, + onError: (error) async { + emit(ErrorAiWriterState(state.command, error: error)); + }, + ); + + if (stream != null) { + emit( + GeneratingAiWriterState( + command, + taskId: stream.$1, + ), + ); + } + } + + void runCommand( + AiWriterCommand command, + PredefinedFormat? predefinedFormat, { + bool isImmediateRun = false, + bool isRetry = false, + }) async { + switch (command) { + case AiWriterCommand.continueWriting: + await _startContinueWriting( + command, + predefinedFormat, + isImmediateRun: isImmediateRun, + ); + break; + case AiWriterCommand.fixSpellingAndGrammar: + case AiWriterCommand.improveWriting: + case AiWriterCommand.makeLonger: + case AiWriterCommand.makeShorter: + await _startSuggestingEdits(command, predefinedFormat); + break; + case AiWriterCommand.explain: + await _startInforming(command, predefinedFormat); + break; + case AiWriterCommand.userQuestion: + if (isRetry && _previousPrompt != null) { + submit(_previousPrompt!.$1, _previousPrompt!.$2); + } + break; + } + } + + void stopStream() async { + if (state is! GeneratingAiWriterState) { + return; + } + final generatingState = state as GeneratingAiWriterState; + await AIEventStopCompleteText( + CompleteTextTaskPB( + taskId: generatingState.taskId, + ), + ).send(); + emit( + ReadyAiWriterState( + state.command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); + } + + void exit() async { + await _textRobot.discard(); + final selection = getAiWriterNode().aiWriterSelection; + if (selection == null) { + return; + } + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.clear, + ); + await editorState.apply( + transaction, + options: const ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + withUpdateSelection: false, + ); + await removeAiWriterNode(editorState, getAiWriterNode()); + } + + void runResponseAction(SuggestionAction action) async { + if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { + await _textRobot.discard(); + _textRobot.reset(); + runCommand(state.command, null, isRetry: true); + return; + } + + final selection = getAiWriterNode().aiWriterSelection; + if (selection == null) { + return; + } + + if (action case SuggestionAction.discard || SuggestionAction.close) { + await _textRobot.discard(); + + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.clear, + ); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + ); + } + + if (action case SuggestionAction.accept || SuggestionAction.keep) { + await _textRobot.persist(); + + if (state.command.acceptWillReplace) { + final nodes = editorState.getNodesInSelection(selection); + final transaction = editorState.transaction..deleteNodes(nodes); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, + ); + } + } + + if (action case SuggestionAction.insertBelow) { + if (state case final ReadyAiWriterState readyState + when readyState.markdownText.isNotEmpty) { + final transaction = editorState.transaction; + final position = ensurePreviousNodeIsEmptyParagraph( + editorState, + getAiWriterNode(), + transaction, + ); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); + _textRobot.start(position: position); + await _textRobot.persist(markdownText: readyState.markdownText); + } else { + await _textRobot.persist(); + } + + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.clear, + ); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, + ); + } + + await removeAiWriterNode(editorState, getAiWriterNode()); + } + + bool hasUnusedResponse() { + return switch (state) { + ReadyAiWriterState( + isFirstRun: final isInitial, + markdownText: final markdownText, + ) => + !isInitial && (markdownText.isNotEmpty || _textRobot.hasAnyResult), + GeneratingAiWriterState() => true, + _ => false, + }; + } + + Future _startContinueWriting( + AiWriterCommand command, + PredefinedFormat? predefinedFormat, { + required bool isImmediateRun, + }) async { + final node = getAiWriterNode(); + + final cursorPosition = getAiWriterNode().aiWriterSelection?.start; + if (cursorPosition == null) { + return; + } + final selection = Selection( + start: Position(path: [0]), + end: cursorPosition, + ).normalized; + + final text = await editorState.getMarkdownInSelection(selection); + if (text.isEmpty) { + if (state is! ReadyAiWriterState) { + return; + } + final readyState = state as ReadyAiWriterState; + emit( + SingleShotAiWriterState( + command, + title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: + LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(), + onDismiss: () { + if (isImmediateRun) { + removeAiWriterNode(editorState, node); + } + }, + ), + ); + emit(readyState); + return; + } + + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: await editorState.getMarkdownInSelection(selection), + completionType: command.toCompletionType(), + onStart: () async { + final transaction = editorState.transaction; + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); + _textRobot.start(position: position); + }, + onProcess: (text) async { + await _textRobot.appendMarkdownText( + text, + attributes: ApplySuggestionFormatType.replace.attributes, + ); + }, + onEnd: () async { + editorState.service.keyboardService?.enable(); + if (state case GeneratingAiWriterState _) { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit(ReadyAiWriterState(command, isFirstRun: false)); + } + }, + onError: (error) async { + editorState.service.keyboardService?.enable(); + emit(ErrorAiWriterState(command, error: error)); + }, + ); + if (stream != null) { + emit( + GeneratingAiWriterState(command, taskId: stream.$1), + ); + } + } + + Future _startSuggestingEdits( + AiWriterCommand command, + PredefinedFormat? predefinedFormat, + ) async { + final node = getAiWriterNode(); + final selection = node.aiWriterSelection; + if (selection == null) { + return; + } + + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: await editorState.getMarkdownInSelection(selection), + completionType: command.toCompletionType(), + onStart: () async { + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.original, + ); + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); + _textRobot.start(position: position); + }, + onProcess: (text) async { + await _textRobot.appendMarkdownText( + text, + attributes: ApplySuggestionFormatType.replace.attributes, + ); + }, + onEnd: () async { + if (state is GeneratingAiWriterState) { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit( + ReadyAiWriterState( + command, + isFirstRun: false, + ), + ); + } + }, + onError: (error) async { + editorState.service.keyboardService?.enable(); + emit(ErrorAiWriterState(command, error: error)); + }, + ); + if (stream != null) { + emit( + GeneratingAiWriterState(command, taskId: stream.$1), + ); + } + } + + Future _startInforming( + AiWriterCommand command, + PredefinedFormat? predefinedFormat, + ) async { + final node = getAiWriterNode(); + final selection = node.aiWriterSelection; + if (selection == null) { + return; + } + + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: await editorState.getMarkdownInSelection(selection), + completionType: command.toCompletionType(), + onStart: () async {}, + onProcess: (text) async { + if (state case final GeneratingAiWriterState generatingState) { + emit( + GeneratingAiWriterState( + command, + taskId: generatingState.taskId, + markdownText: generatingState.markdownText + text, + ), + ); + } + }, + onEnd: () async { + editorState.service.keyboardService?.enable(); + if (state case final GeneratingAiWriterState generatingState) { + emit( + ReadyAiWriterState( + command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); + } + }, + onError: (error) async { + emit(ErrorAiWriterState(command, error: error)); + }, + ); + if (stream != null) { + emit( + GeneratingAiWriterState(command, taskId: stream.$1), + ); + } + } +} + +sealed class AiWriterState { + const AiWriterState(this.command); + + final AiWriterCommand command; +} + +class ReadyAiWriterState extends AiWriterState { + const ReadyAiWriterState( + super.command, { + required this.isFirstRun, + this.markdownText = '', + }); + + final bool isFirstRun; + final String markdownText; +} + +class GeneratingAiWriterState extends AiWriterState { + const GeneratingAiWriterState( + super.command, { + required this.taskId, + this.progress = '', + this.markdownText = '', + }); + + final String taskId; + final String progress; + final String markdownText; +} + +class ErrorAiWriterState extends AiWriterState { + const ErrorAiWriterState( + super.command, { + required this.error, + }); + + final AIError error; +} + +class SingleShotAiWriterState extends AiWriterState { + const SingleShotAiWriterState( + super.command, { + required this.title, + required this.description, + required this.onDismiss, + }); + + final String title; + final String description; + final void Function() onDismiss; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart new file mode 100644 index 0000000000..349d7cb419 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -0,0 +1,131 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../ai_writer_block_component.dart'; + +const kdefaultReplacementType = AskAIReplacementType.markdown; + +enum AskAIReplacementType { + markdown, + plainText, +} + +enum SuggestionAction { + accept, + discard, + close, + tryAgain, + rewrite, + keep, + insertBelow; + + String get i18n => switch (this) { + accept => LocaleKeys.suggestion_accept.tr(), + discard => LocaleKeys.suggestion_discard.tr(), + close => LocaleKeys.suggestion_close.tr(), + tryAgain => LocaleKeys.suggestion_tryAgain.tr(), + rewrite => LocaleKeys.suggestion_rewrite.tr(), + keep => LocaleKeys.suggestion_keep.tr(), + insertBelow => LocaleKeys.suggestion_insertBelow.tr(), + }; + + FlowySvg buildIcon(BuildContext context) { + final icon = switch (this) { + accept || keep => FlowySvgs.ai_fix_spelling_grammar_s, + discard || close => FlowySvgs.toast_close_s, + tryAgain || rewrite => FlowySvgs.ai_try_again_s, + insertBelow => FlowySvgs.suggestion_insert_below_s, + }; + + return FlowySvg( + icon, + size: Size.square(16.0), + color: switch (this) { + accept || keep => Color(0xFF278E42), + discard || close => Color(0xFFC40055), + _ => Theme.of(context).iconTheme.color, + }, + ); + } +} + +enum AiWriterCommand { + userQuestion, + explain, + // summarize, + continueWriting, + fixSpellingAndGrammar, + improveWriting, + makeShorter, + makeLonger; + + String defaultPrompt(String input) => switch (this) { + userQuestion => input, + explain => "Explain this phrase in a concise manner:\n\n$input", + // summarize => '$input\n\nTl;dr', + continueWriting => + 'Continue writing based on this existing text:\n\n$input', + fixSpellingAndGrammar => 'Correct this to standard English:\n\n$input', + improveWriting => 'Rewrite this in your own words:\n\n$input', + makeShorter => 'Make this text shorter:\n\n$input', + makeLonger => 'Make this text longer:\n\n$input', + }; + + String get i18n => switch (this) { + userQuestion => LocaleKeys.document_plugins_aiWriter_userQuestion.tr(), + explain => LocaleKeys.document_plugins_aiWriter_explain.tr(), + // summarize => LocaleKeys.document_plugins_aiWriter_summarize.tr(), + continueWriting => + LocaleKeys.document_plugins_aiWriter_continueWriting.tr(), + fixSpellingAndGrammar => + LocaleKeys.document_plugins_aiWriter_fixSpelling.tr(), + improveWriting => + LocaleKeys.document_plugins_smartEditImproveWriting.tr(), + makeShorter => LocaleKeys.document_plugins_aiWriter_makeShorter.tr(), + makeLonger => LocaleKeys.document_plugins_aiWriter_makeLonger.tr(), + }; + + FlowySvgData get icon => switch (this) { + userQuestion => FlowySvgs.ai_sparks_s, + explain => FlowySvgs.ai_explain_s, + // summarize => FlowySvgs.ai_summarize_s, + continueWriting || improveWriting => FlowySvgs.ai_improve_writing_s, + fixSpellingAndGrammar => FlowySvgs.ai_fix_spelling_grammar_s, + makeShorter => FlowySvgs.ai_make_shorter_s, + makeLonger => FlowySvgs.ai_make_longer_s, + }; + + CompletionTypePB toCompletionType() => switch (this) { + userQuestion => CompletionTypePB.UserQuestion, + explain => CompletionTypePB.ExplainSelected, + // summarize => CompletionTypePB.Summarize, + continueWriting => CompletionTypePB.ContinueWriting, + fixSpellingAndGrammar => CompletionTypePB.SpellingAndGrammar, + improveWriting => CompletionTypePB.ImproveWriting, + makeShorter => CompletionTypePB.MakeShorter, + makeLonger => CompletionTypePB.MakeLonger, + }; + + bool get acceptWillReplace => switch (this) { + AiWriterCommand.fixSpellingAndGrammar || + AiWriterCommand.improveWriting || + AiWriterCommand.makeLonger || + AiWriterCommand.makeShorter => + true, + _ => false, + }; +} + +enum ApplySuggestionFormatType { + original(AiWriterBlockKeys.suggestionOriginal), + replace(AiWriterBlockKeys.suggestionReplacement), + clear(null); + + const ApplySuggestionFormatType(this.value); + final String? value; + + Map get attributes => {AiWriterBlockKeys.suggestion: value}; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart similarity index 74% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 9ed5b046b9..1335f51df5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/ask_ai_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -1,7 +1,29 @@ import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -extension AskAINodeExtension on EditorState { +import '../ai_writer_block_component.dart'; +import 'ai_writer_entities.dart'; + +extension AiWriterExtension on Node { + bool get isAiWriterInitialized { + return attributes[AiWriterBlockKeys.isInitialized]; + } + + Selection? get aiWriterSelection { + final selection = attributes[AiWriterBlockKeys.selection]; + if (selection == null) { + return null; + } + return Selection.fromJson(selection); + } + + AiWriterCommand get aiWriterCommand { + final index = attributes[AiWriterBlockKeys.command]; + return AiWriterCommand.values[index]; + } +} + +extension AiWriterNodeExtension on EditorState { Future getMarkdownInSelection(Selection? selection) async { selection ??= this.selection?.normalized; if (selection == null || selection.isCollapsed) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart new file mode 100644 index 0000000000..a16fc44641 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart @@ -0,0 +1,63 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'operations/ai_writer_entities.dart'; + +class SuggestionActionBar extends StatelessWidget { + const SuggestionActionBar({ + super.key, + required this.actions, + required this.onTap, + }); + + final List actions; + final void Function(SuggestionAction) onTap; + + @override + Widget build(BuildContext context) { + return SeparatedRow( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const HSpace(4.0), + children: actions + .map( + (action) => SuggestionActionButton( + action: action, + onTap: () => onTap(action), + ), + ) + .toList(), + ); + } +} + +class SuggestionActionButton extends StatelessWidget { + const SuggestionActionButton({ + super.key, + required this.action, + required this.onTap, + }); + + final SuggestionAction action; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: FlowyButton( + text: FlowyText( + action.i18n, + figmaLineHeight: 20, + ), + leftIcon: action.buildIcon(context), + iconPadding: 4.0, + margin: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 4.0, + ), + onTap: onTap, + useIntrinsicWidth: true, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/learn_more_action.dart deleted file mode 100644 index 17e89b1bca..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/util/learn_more_action.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; - -const String learnMoreUrl = - 'https://docs.appflowy.io/docs/appflowy/product/appflowy-x-openai'; - -Future openLearnMorePage() async { - await afLaunchUrlString(learnMoreUrl); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_limit_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_limit_dialog.dart deleted file mode 100644 index 3a57a7f49c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_limit_dialog.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:easy_localization/easy_localization.dart'; - -void showAILimitDialog(BuildContext context, String message) { - showConfirmDialog( - context: context, - title: LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(), - description: message, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_operations.dart deleted file mode 100644 index 0c2fabfb50..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_operations.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// Notes: All the operation related to the AI writer block will be applied -/// in memory. -class AIWriterBlockOperations { - AIWriterBlockOperations({ - required this.editorState, - required this.aiWriterNode, - }) : assert(aiWriterNode.type == AIWriterBlockKeys.type); - - final EditorState editorState; - final Node aiWriterNode; - - /// Update the prompt text in the node. - Future updatePromptText(String prompt) async { - final transaction = editorState.transaction; - transaction.updateNode( - aiWriterNode, - {AIWriterBlockKeys.prompt: prompt}, - ); - await editorState.apply( - transaction, - options: const ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - ); - } - - /// Update the generation count in the node. - Future updateGenerationCount(int count) async { - final transaction = editorState.transaction; - transaction.updateNode( - aiWriterNode, - {AIWriterBlockKeys.generationCount: count}, - ); - await editorState.apply( - transaction, - options: const ApplyOptions(inMemoryUpdate: true), - ); - } - - /// Ensure the previous node is a empty paragraph node without any styles. - Future ensurePreviousNodeIsEmptyParagraphNode() async { - final previous = aiWriterNode.previous; - final Selection selection; - - // 1. previous node is null or - // 2. previous node is not a paragraph node or - // 3. previous node is a paragraph node but not empty - final isNotEmptyParagraphNode = previous == null || - previous.type != ParagraphBlockKeys.type || - (previous.delta?.toPlainText().isNotEmpty ?? false); - - if (isNotEmptyParagraphNode) { - final path = aiWriterNode.path; - final transaction = editorState.transaction; - selection = Selection.collapsed(Position(path: path)); - transaction - ..insertNode( - path, - paragraphNode(), - ) - ..afterSelection = selection; - await editorState.apply(transaction); - } else { - selection = Selection.collapsed(Position(path: previous.path)); - } - - final transaction = editorState.transaction; - transaction.updateNode(aiWriterNode, { - AIWriterBlockKeys.startSelection: selection.toJson(), - }); - transaction.afterSelection = selection; - await editorState.apply( - transaction, - options: const ApplyOptions(inMemoryUpdate: true), - ); - } - - /// Discard the current response and delete the previous node. - Future discardCurrentResponse({ - required Node aiWriterNode, - Selection? selection, - }) async { - if (selection != null) { - final start = selection.start.path; - final end = aiWriterNode.previous?.path; - if (end != null) { - final transaction = editorState.transaction; - transaction.deleteNodesAtPath( - start, - end.last - start.last + 1, - ); - await editorState.apply(transaction); - await ensurePreviousNodeIsEmptyParagraphNode(); - } - } - } - - /// Remove the ai writer node from the editor. - Future removeAIWriterNode(Node aiWriterNode) async { - final transaction = editorState.transaction; - transaction.deleteNode(aiWriterNode); - await editorState.apply( - transaction, - options: const ApplyOptions(inMemoryUpdate: true, recordUndo: false), - withUpdateSelection: false, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_widgets.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_widgets.dart deleted file mode 100644 index b6681ed7fd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_block_widgets.dart +++ /dev/null @@ -1,104 +0,0 @@ -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 AIWriterBlockHeader extends StatelessWidget { - const AIWriterBlockHeader({super.key}); - - @override - Widget build(BuildContext context) { - return FlowyText.medium( - LocaleKeys.document_plugins_autoGeneratorTitleName.tr(), - fontSize: 14, - ); - } -} - -class AIWriterBlockInputField extends StatelessWidget { - const AIWriterBlockInputField({ - super.key, - required this.onGenerate, - required this.onExit, - }); - - final VoidCallback onGenerate; - final VoidCallback onExit; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - PrimaryRoundedButton( - text: LocaleKeys.button_generate.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 10.0, - ), - radius: 8.0, - onTap: onGenerate, - ), - const Space(10, 0), - OutlinedRoundedButton( - text: LocaleKeys.button_cancel.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 10.0, - ), - onTap: onExit, - ), - Flexible( - child: Container( - alignment: Alignment.centerRight, - child: FlowyText.regular( - LocaleKeys.document_plugins_warning.tr(), - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - fontSize: 12, - ), - ), - ), - ], - ); - } -} - -class AIWriterBlockFooter extends StatelessWidget { - const AIWriterBlockFooter({ - super.key, - required this.onKeep, - required this.onRewrite, - required this.onDiscard, - }); - - final VoidCallback onKeep; - final VoidCallback onRewrite; - final VoidCallback onDiscard; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - PrimaryRoundedButton( - text: LocaleKeys.button_keep.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 9.0, - ), - onTap: onKeep, - ), - const HSpace(10), - OutlinedRoundedButton( - text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(), - onTap: onRewrite, - ), - const HSpace(10), - OutlinedRoundedButton( - text: LocaleKeys.button_discard.tr(), - onTap: onDiscard, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action.dart deleted file mode 100644 index dfcec0318e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:flutter/material.dart'; - -class AskAIActionWrapper extends ActionCell { - AskAIActionWrapper(this.inner); - - final AskAIAction inner; - - Widget? icon(Color iconColor) => null; - - @override - String get name => inner.name; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart deleted file mode 100644 index 1fcd8d65ed..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart +++ /dev/null @@ -1,299 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/service/ai_client.dart'; -import 'package:appflowy/ai/service/error.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'ask_ai_action_bloc.freezed.dart'; - -enum AskAIReplacementType { - markdown, - plainText, -} - -const _defaultReplacementType = AskAIReplacementType.markdown; - -class AskAIActionBloc extends Bloc { - AskAIActionBloc({ - required this.node, - required this.editorState, - required this.action, - required this.objectId, - this.enableLogging = true, - }) : super( - AskAIState.initial(action), - ) { - on((event, emit) async { - await event.when( - initial: (aiRepositoryProvider) async { - aiRepository = await aiRepositoryProvider; - aiRepositoryCompleter.complete(); - }, - started: () async { - await _requestCompletions(); - }, - rewrite: () async { - await _requestCompletions(rewrite: true); - }, - replace: () async { - await _replace(); - await _exit(); - }, - insertBelow: () async { - await _insertBelow(); - await _exit(); - }, - cancel: () async { - isCanceled = true; - await _exit(); - }, - update: (result, isLoading, aiError) { - emit( - state.copyWith( - result: result, - loading: isLoading, - requestError: aiError, - ), - ); - }, - ); - }); - } - - final Node node; - final EditorState editorState; - final AskAIAction action; - final bool enableLogging; - // used to wait for the aiRepository to be initialized - final aiRepositoryCompleter = Completer(); - late final AIRepository aiRepository; - final String objectId; - - bool isCanceled = false; - - Future _requestCompletions({ - bool rewrite = false, - }) async { - await aiRepositoryCompleter.future; - - if (rewrite) { - add(const AskAIEvent.update('', true, null)); - } - - if (enableLogging) { - Log.info('[smart_edit] request completions'); - } - - final content = node.attributes[AskAIBlockKeys.content] as String; - await aiRepository.streamCompletion( - objectId: objectId, - text: content, - completionType: completionTypeFromInt(state.action), - onStart: () async { - if (isCanceled) { - return; - } - if (enableLogging) { - Log.info('[smart_edit] start generating'); - } - add(const AskAIEvent.update('', true, null)); - }, - onProcess: (text) async { - if (isCanceled) { - return; - } - // only display the log in debug mode - if (enableLogging) { - Log.debug('[smart_edit] onProcess: $text'); - } - final newResult = state.result + text; - add(AskAIEvent.update(newResult, false, null)); - }, - onEnd: () async { - if (isCanceled) { - return; - } - if (enableLogging) { - Log.info('[smart_edit] end generating'); - } - add(AskAIEvent.update('${state.result}\n', false, null)); - }, - onError: (error) async { - if (isCanceled) { - return; - } - if (enableLogging) { - Log.info('[smart_edit] onError: $error'); - } - add(AskAIEvent.update('', false, error)); - await _exit(); - await _clearSelection(); - }, - ); - } - - Future _insertBelow() async { - // check the selection is not empty - final selection = editorState.selection?.normalized; - if (selection == null) { - return; - } - final nodes = customMarkdownToDocument(state.result) - .root - .children - .map((e) => e.deepCopy()) - .toList(); - final insertedPath = selection.end.path.next; - final transaction = editorState.transaction; - transaction.insertNodes( - insertedPath, - nodes, - ); - final lastDeltaLength = nodes.lastOrNull?.delta?.length ?? 0; - transaction.afterSelection = Selection( - start: Position(path: insertedPath), - end: Position( - path: insertedPath.nextNPath(nodes.length - 1), - offset: lastDeltaLength, - ), - ); - await editorState.apply(transaction); - } - - Future _replace() async { - switch (_defaultReplacementType) { - case AskAIReplacementType.markdown: - await _replaceWithMarkdown(); - case AskAIReplacementType.plainText: - await _replaceWithPlainText(); - } - } - - Future _replaceWithMarkdown() async { - final selection = editorState.selection?.normalized; - if (selection == null) { - return; - } - - final nodes = customMarkdownToDocument(state.result) - .root - .children - .map((e) => e.deepCopy()) - .toList(); - if (nodes.isEmpty) { - return; - } - - final nodesInSelection = editorState.getNodesInSelection(selection); - final transaction = editorState.transaction; - transaction.insertNodes( - selection.start.path, - nodes, - ); - transaction.deleteNodes(nodesInSelection); - transaction.afterSelection = Selection( - start: selection.start, - end: Position( - path: selection.start.path.nextNPath(nodes.length - 1), - offset: nodes.lastOrNull?.delta?.length ?? 0, - ), - ); - await editorState.apply(transaction); - } - - Future _replaceWithPlainText() async { - final result = state.result.trim(); - if (result.isEmpty) { - return; - } - - final selection = editorState.selection?.normalized; - if (selection == null) { - return; - } - final nodes = editorState.getNodesInSelection(selection); - if (nodes.isEmpty || !nodes.every((element) => element.delta != null)) { - return; - } - - final replaceTexts = result.split('\n') - ..removeWhere((element) => element.isEmpty); - final transaction = editorState.transaction; - transaction.replaceTexts( - nodes, - selection, - replaceTexts, - ); - await editorState.apply(transaction); - - int endOffset = replaceTexts.last.length; - if (replaceTexts.length == 1) { - endOffset += selection.start.offset; - } - final end = Position( - path: [selection.start.path.first + replaceTexts.length - 1], - offset: endOffset, - ); - editorState.selection = Selection( - start: selection.start, - end: end, - ); - } - - Future _exit() async { - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - ), - ); - } - - Future _clearSelection() async { - final selection = editorState.selection; - if (selection == null) { - return; - } - editorState.selection = null; - } -} - -@freezed -class AskAIEvent with _$AskAIEvent { - const factory AskAIEvent.initial( - Future aiRepositoryProvider, - ) = _Initial; - const factory AskAIEvent.started() = _Started; - const factory AskAIEvent.rewrite() = _Rewrite; - const factory AskAIEvent.replace() = _Replace; - const factory AskAIEvent.insertBelow() = _InsertBelow; - const factory AskAIEvent.cancel() = _Cancel; - const factory AskAIEvent.update( - String result, - bool isLoading, - AIError? error, - ) = _Update; -} - -@freezed -class AskAIState with _$AskAIState { - const factory AskAIState({ - required bool loading, - required String result, - required AskAIAction action, - @Default(null) AIError? requestError, - }) = _AskAIState; - - factory AskAIState.initial(AskAIAction action) => AskAIState( - loading: true, - action: action, - result: '', - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_block_widgets.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_block_widgets.dart deleted file mode 100644 index d28f416d1b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_block_widgets.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class AskAIInputContent extends StatelessWidget { - const AskAIInputContent({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Card( - elevation: 5, - color: Theme.of(context).colorScheme.surface, - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Container( - margin: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - state.action.name, - fontSize: 14, - ), - const VSpace(16), - state.loading - ? _buildLoadingWidget(context) - : _buildResultWidget(context, state), - const VSpace(16), - const AskAIFooter(), - ], - ), - ), - ); - }, - ); - } - - Widget _buildResultWidget(BuildContext context, AskAIState state) { - return Flexible( - child: AIMarkdownText( - markdown: state.result, - ), - ); - } - - Widget _buildLoadingWidget(BuildContext context) { - return const Padding( - padding: EdgeInsets.symmetric(horizontal: 4.0), - child: SizedBox.square( - dimension: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - ); - } -} - -class AskAIFooter extends StatelessWidget { - const AskAIFooter({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - OutlinedRoundedButton( - text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(), - onTap: () => - context.read().add(const AskAIEvent.rewrite()), - ), - const HSpace(10), - OutlinedRoundedButton( - text: LocaleKeys.button_replace.tr(), - onTap: () => - context.read().add(const AskAIEvent.replace()), - ), - const HSpace(10), - OutlinedRoundedButton( - text: LocaleKeys.button_insertBelow.tr(), - onTap: () => context - .read() - .add(const AskAIEvent.insertBelow()), - ), - const HSpace(10), - OutlinedRoundedButton( - text: LocaleKeys.button_cancel.tr(), - onTap: () => - context.read().add(const AskAIEvent.cancel()), - ), - Expanded( - child: Container( - alignment: Alignment.centerRight, - child: Text( - LocaleKeys.document_plugins_warning.tr(), - style: TextStyle(color: Theme.of(context).hintColor), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/barrier_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/barrier_dialog.dart deleted file mode 100644 index 69f453dcfc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/barrier_dialog.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class BarrierDialog { - BarrierDialog(this.context); - - late BuildContext loadingContext; - final BuildContext context; - - void show() => showDialog( - context: context, - barrierDismissible: false, - barrierColor: Colors.transparent, - builder: (context) { - loadingContext = context; - return const SizedBox.shrink(); - }, - ); - - void dismiss() => Navigator.of(loadingContext).pop(); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/discard_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/discard_dialog.dart deleted file mode 100644 index 4c0c2bc91a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/discard_dialog.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; - -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -import 'package:easy_localization/easy_localization.dart'; - -class DiscardDialog extends StatelessWidget { - const DiscardDialog({ - super.key, - required this.onConfirm, - required this.onCancel, - }); - - final VoidCallback onConfirm; - final VoidCallback onCancel; - - @override - Widget build(BuildContext context) { - return NavigatorOkCancelDialog( - message: LocaleKeys.document_plugins_discardResponse.tr(), - okTitle: LocaleKeys.button_discard.tr(), - cancelTitle: LocaleKeys.button_cancel.tr(), - onOkPressed: onConfirm, - onCancelPressed: onCancel, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 0cc9a50e5c..ac170fef00 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -3,127 +3,205 @@ import 'dart:convert'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/foundation.dart'; import 'package:synchronized/synchronized.dart'; +const _enableDebug = false; + class MarkdownTextRobot { MarkdownTextRobot({ required this.editorState, - this.enableDebug = true, }); final EditorState editorState; - final bool enableDebug; - final Lock lock = Lock(); + final Lock _lock = Lock(); - // The selection before the text robot is ready. - Selection? _startSelection; + /// The text position where new nodes will be inserted + Position? _insertPosition; - // The markdown text to be inserted. + /// The markdown text to be inserted String _markdownText = ''; - // Only for debug. Enable by [enableDebug]. - @visibleForTesting - final List debugMarkdownTexts = []; + /// The nodes inserted in the previous refresh. + Iterable _insertedNodes = []; - // The nodes inserted in the previous refresh. - Iterable _previousInsertedNodes = []; + /// Only for debug via [_enableDebug]. + final List _debugMarkdownTexts = []; - /// Start the text robot. - /// - /// Must call this function before using the text robot. - void start() { - _startSelection = editorState.selection; + bool get hasAnyResult => _markdownText.isNotEmpty; - if (enableDebug) { + Selection? getInsertedSelection() { + final position = _insertPosition; + if (position == null) { + Log.error("Expected non-null insert markdown text position"); + return null; + } + + if (_insertedNodes.isEmpty) { + return Selection.collapsed(position); + } + return Selection( + start: position, + end: Position( + path: position.path.nextNPath(_insertedNodes.length - 1), + ), + ); + } + + List getInsertedNodes() { + final selection = getInsertedSelection(); + return selection == null ? [] : editorState.getNodesInSelection(selection); + } + + void start({ + Position? position, + }) { + _insertPosition ??= position ?? editorState.selection?.start; + + if (_enableDebug) { Log.info( - 'MarkdownTextRobot prepare, current selection: $_startSelection', + 'MarkdownTextRobot start with insert text position: $_insertPosition', ); } } - /// Append the markdown text to the text robot. - /// - /// The text will be inserted into document but not persisted until the text - /// robot is stopped. - Future appendMarkdownText(String text) async { + /// The text will be inserted into the document but only in memory + Future appendMarkdownText( + String text, { + Map? attributes, + }) async { _markdownText += text; - await lock.synchronized(() async { - await _refresh(); + await _lock.synchronized(() async { + await _refresh( + inMemoryUpdate: true, + attributes: attributes, + ); }); - if (enableDebug) { - debugMarkdownTexts.add(text); - Log.info('debug markdown texts: ${jsonEncode(debugMarkdownTexts)}'); + if (_enableDebug) { + _debugMarkdownTexts.add(text); + Log.info( + 'MarkdownTextRobot receive markdown: ${jsonEncode(_debugMarkdownTexts)}', + ); } } - /// Stop the text robot. - /// - /// The text will be persisted into document. - Future stop() async { - // persist the markdown text - await lock.synchronized(() async { + Future stop({ + Map? attributes, + }) async { + await _lock.synchronized(() async { + await _refresh( + inMemoryUpdate: true, + updateSelection: false, + attributes: attributes, + ); + }); + } + + /// Persist the text into the document + Future persist({String? markdownText}) async { + if (markdownText != null) { + _markdownText = markdownText; + } + await _lock.synchronized(() async { await _refresh(inMemoryUpdate: false); }); - _markdownText = ''; - - if (enableDebug) { - Log.info( - 'debug markdown texts: ${jsonEncode(debugMarkdownTexts)}', - ); - debugMarkdownTexts.clear(); + if (_enableDebug) { + Log.info('MarkdownTextRobot stop'); + _debugMarkdownTexts.clear(); } } - /// Refreshes the editor state with the current markdown text by: - /// - /// 1. Converting markdown to document nodes - /// 2. Replacing previously inserted nodes with new nodes - /// 3. Updating selection position - Future _refresh({bool inMemoryUpdate = true}) async { - final start = _startSelection?.start; + /// Discard the inserted content + Future discard() async { + final start = _insertPosition; if (start == null) { return; } - - final transaction = editorState.transaction; - - // Convert markdown and deep copy nodes - final nodes = customMarkdownToDocument(_markdownText).root.children.map( - (node) => node.deepCopy(), - ); // deep copy the nodes to avoid the linked entities being changed. - - // Insert new nodes at selection start - transaction.insertNodes(start.path, nodes); - - // Remove previously inserted nodes if they exist - if (_previousInsertedNodes.isNotEmpty) { - // fallback to the calculated position if the selection is null. - final end = editorState.selection?.end ?? - Position( - path: start.path.nextNPath(_previousInsertedNodes.length - 1), - ); - final deletedNodes = editorState.getNodesInSelection( - Selection(start: start, end: end), - ); - transaction.deleteNodes(deletedNodes); + if (_insertedNodes.isEmpty) { + return; } - // Update selection to end of inserted content if it contains text - final lastDelta = nodes.lastOrNull?.delta; + // fallback to the calculated position if the selection is null. + final end = Position( + path: start.path.nextNPath(_insertedNodes.length - 1), + ); + final deletedNodes = editorState.getNodesInSelection( + Selection(start: start, end: end), + ); + final transaction = editorState.transaction + ..deleteNodes(deletedNodes) + ..afterSelection = Selection.collapsed(start); + + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + ); + + if (_enableDebug) { + Log.info('MarkdownTextRobot discard'); + } + } + + void reset() { + _markdownText = ''; + _insertedNodes = []; + _insertPosition = null; + } + + Future _refresh({ + required bool inMemoryUpdate, + bool updateSelection = true, + Map? attributes, + }) async { + final position = _insertPosition; + if (position == null) { + Log.error("Expected non-null insert markdown text position"); + return; + } + + final node = editorState.getNodeAtPath(position.path); + if (node == null) { + Log.error("Cannot find node at position: ${position.path}"); + return; + } + + // Convert markdown and deep copy the nodes, prevent ing the linked + // entities from being changed + final documentNodes = customMarkdownToDocument( + _markdownText, + tableWidth: 250.0, + ).root.children; + + final newNodes = attributes == null + ? documentNodes + : documentNodes + .map((node) => _styleDelta(node: node, attributes: attributes)) + .toList(); + + if (newNodes.isEmpty) { + return; + } + final transaction = editorState.transaction + ..insertNodes(position.path, newNodes) + ..deleteNodes(getInsertedNodes()); + + final lastDelta = newNodes.lastOrNull?.delta; if (lastDelta != null) { transaction.afterSelection = Selection.collapsed( Position( - path: start.path.nextNPath(nodes.length - 1), + path: position.path.nextNPath(newNodes.length - 1), offset: lastDelta.length, ), ); } + if (!updateSelection) { + transaction.afterSelection = null; + } + await editorState.apply( transaction, options: ApplyOptions( @@ -132,6 +210,32 @@ class MarkdownTextRobot { ), ); - _previousInsertedNodes = nodes; + _insertedNodes = newNodes; + } + + Node _styleDelta({ + required Node node, + required Map attributes, + }) { + if (node.delta != null) { + final delta = node.delta!; + final attributeDelta = Delta() + ..retain(delta.length, attributes: attributes); + final newDelta = delta.compose(attributeDelta); + final newAttributes = node.attributes; + newAttributes['delta'] = newDelta.toJson(); + node.updateAttributes(newAttributes); + } + + List? children; + if (node.children.isNotEmpty) { + children = node.children + .map((child) => _styleDelta(node: node, attributes: attributes)) + .toList(); + } + + return node.copyWith( + children: children, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index c37a85260e..8a475fd5a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -1,8 +1,7 @@ export 'actions/block_action_list.dart'; export 'actions/option/option_actions.dart'; export 'ai/ai_writer_block_component.dart'; -export 'ai/ask_ai_block_component.dart'; -export 'ai/ask_ai_toolbar_item.dart'; +export 'ai/ai_writer_toolbar_item.dart'; export 'align_toolbar_item/align_toolbar_item.dart'; export 'base/backtick_character_command.dart'; export 'base/cover_title_command.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart index 4e3f455522..46d1b4eabb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -15,41 +15,67 @@ final _keywords = [ 'autogenerator', ]; -// auto generate menu item SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem( getName: LocaleKeys.document_slashMenu_name_aiWriter.tr, - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertAiWriter(), + keywords: [ + ..._keywords, + LocaleKeys.document_slashMenu_name_aiWriter.tr(), + ], + handler: (editorState, _, __) async => + _insertAiWriter(editorState, AiWriterCommand.userQuestion), icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_ai_writer_s, + data: AiWriterCommand.userQuestion.icon, isSelected: isSelected, style: style, ), nameBuilder: slashMenuItemNameBuilder, ); -extension on EditorState { - Future insertAiWriter() async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final node = getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final newNode = aiWriterNode(start: selection); +SelectionMenuItem continueWritingSlashMenuItem = SelectionMenuItem( + getName: LocaleKeys.document_plugins_aiWriter_continueWriting.tr, + keywords: [ + ..._keywords, + LocaleKeys.document_plugins_aiWriter_continueWriting.tr(), + ], + handler: (editorState, _, __) async => + _insertAiWriter(editorState, AiWriterCommand.continueWriting), + icon: (_, isSelected, style) => SelectableSvgWidget( + data: AiWriterCommand.continueWriting.icon, + isSelected: isSelected, + style: style, + ), + nameBuilder: slashMenuItemNameBuilder, +); - final transaction = this.transaction; - //default insert after - final path = node.path.next; - transaction - ..insertNode(path, newNode) - ..afterSelection = null; - await apply( - transaction, - options: const ApplyOptions(inMemoryUpdate: true), - ); +Future _insertAiWriter( + EditorState editorState, + AiWriterCommand action, +) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; } + + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null || node.delta == null) { + return; + } + final newNode = aiWriterNode( + selection: selection, + command: action, + ); + + // default insert after + final path = node.path.next; + final transaction = editorState.transaction + ..insertNode(path, newNode) + ..afterSelection = null; + + await editorState.apply( + transaction, + options: const ApplyOptions( + recordUndo: false, + inMemoryUpdate: true, + ), + ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart index eb4ce5946b..e953694966 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart @@ -50,8 +50,11 @@ List _defaultSlashMenuItems({ DocumentBloc? documentBloc, }) { return [ - // disable ai writer in local mode - if (!isLocalMode) aiWriterSlashMenuItem, + // ai + if (!isLocalMode) ...[ + continueWritingSlashMenuItem, + aiWriterSlashMenuItem, + ], paragraphSlashMenuItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 1f32c94cf4..10a5e117af 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -10,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/util/font_family_extension.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; @@ -336,6 +337,11 @@ class EditorStyleCustomizer { return before; } + final suggestion = attributes[AiWriterBlockKeys.suggestion] as String?; + final newStyle = suggestion == null + ? after.style + : _styleSuggestion(after.style, suggestion); + if (attributes.backgroundColor != null) { final color = EditorFontColors.fromBuiltInColors( context, @@ -344,7 +350,7 @@ class EditorStyleCustomizer { if (color != null) { return TextSpan( text: before.text, - style: after.style?.merge( + style: newStyle?.merge( TextStyle(backgroundColor: color), ), ); @@ -359,7 +365,7 @@ class EditorStyleCustomizer { } else { return TextSpan( text: before.text, - style: after.style?.merge( + style: newStyle?.merge( getGoogleFontSafely(attributes.fontFamily!), ), ); @@ -376,7 +382,7 @@ class EditorStyleCustomizer { final type = mention[MentionBlockKeys.type]; return WidgetSpan( alignment: PlaceholderAlignment.middle, - style: after.style, + style: newStyle, child: MentionBlock( key: ValueKey( switch (type) { @@ -388,7 +394,7 @@ class EditorStyleCustomizer { node: node, index: index, mention: mention, - textStyle: after.style, + textStyle: newStyle, ), ); } @@ -451,6 +457,13 @@ class EditorStyleCustomizer { ); } + if (suggestion != null) { + return TextSpan( + text: before.text, + style: newStyle, + ); + } + return defaultTextSpanDecoratorForAttribute( context, node, @@ -543,4 +556,34 @@ class EditorStyleCustomizer { return textSpan; } + + TextStyle? _styleSuggestion(TextStyle? style, String suggestion) { + if (style == null) { + return null; + } + final fontSize = style.fontSize ?? 14.0; + final isLight = Theme.of(context).isLightMode; + final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4); + final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4); + return switch (suggestion) { + AiWriterBlockKeys.suggestionOriginal => style.copyWith( + color: Theme.of(context).disabledColor, + decoration: TextDecoration.lineThrough, + ), + AiWriterBlockKeys.suggestionReplacement => style.copyWith( + color: Colors.transparent, + decoration: TextDecoration.underline, + decorationColor: underlineColor, + decorationThickness: 1.0, + // hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb + shadows: [ + Shadow( + color: textColor, + offset: Offset(0, -fontSize * 0.2), + ), + ], + ), + _ => style, + }; + } } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 64747b4829..621ba988cf 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,5 +1,3 @@ -import 'package:appflowy/ai/service/ai_client.dart'; -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/cloud_env.dart'; @@ -15,7 +13,6 @@ import 'package:appflowy/user/application/auth/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'; -import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; @@ -83,16 +80,6 @@ void _resolveCommonService( () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(), ); - getIt.registerFactoryAsync( - () async { - final result = await UserBackendService.getCurrentUserProfile(); - return result.fold( - (s) => AppFlowyAIService(), - (e) => throw Exception('Failed to get user profile: ${e.msg}'), - ); - }, - ); - getIt.registerFactory( () => ClipboardService(), ); diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart new file mode 100644 index 0000000000..73ad2736a0 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -0,0 +1,411 @@ +import 'dart:async'; + +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../util.dart'; + +const _aiResponse = 'UPDATED:'; + +class _MockCompletionStream extends Mock implements CompletionStream {} + +class _MockAIRepository extends Mock implements AppFlowyAIService { + @override + Future<(String, CompletionStream)?> streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + List sourceIds = const [], + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) async { + final stream = _MockCompletionStream(); + unawaited( + Future(() async { + await onStart(); + final lines = text.split('\n'); + for (final line in lines) { + if (line.isNotEmpty) { + await onProcess('$_aiResponse $line\n\n'); + } + } + await onEnd(); + }), + ); + return ('mock_id', stream); + } +} + +class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { + @override + Future<(String, CompletionStream)?> streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + List sourceIds = const [], + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) async { + final stream = _MockCompletionStream(); + unawaited( + Future(() async { + await onStart(); + // only return 1 line. + await onProcess('Hello World'); + await onEnd(); + }), + ); + return ('mock_id', stream); + } +} + +class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { + @override + Future<(String, CompletionStream)?> streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + List sourceIds = const [], + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) async { + final stream = _MockCompletionStream(); + unawaited( + Future(() async { + await onStart(); + // return 10 lines + for (var i = 0; i < 10; i++) { + await onProcess('Hello World\n\n'); + } + await onEnd(); + }), + ); + return ('mock_id', stream); + } +} + +class _MockErrorRepository extends Mock implements AppFlowyAIService { + @override + Future<(String, CompletionStream)?> streamCompletion({ + String? objectId, + required String text, + PredefinedFormat? format, + List sourceIds = const [], + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) async { + final stream = _MockCompletionStream(); + unawaited( + Future(() async { + await onStart(); + onError( + const AIError( + message: 'Error', + code: AIErrorCode.aiResponseLimitExceeded, + ), + ); + }), + ); + return ('mock_id', stream); + } +} + +void main() { + group('AIWriterCubit:', () { + const text1 = '1. Select text to style using the toolbar menu.'; + const text2 = '2. Discover more styling options in Aa.'; + const text3 = + '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + blocTest( + 'send request before the bloc is initialized', + build: () { + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + ], + ), + ); + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final command = AiWriterCommand.explain; + final node = aiWriterNode( + command: command, + selection: selection, + ); + return AiWriterCubit( + documentId: '', + getAiWriterNode: () => node, + editorState: editorState, + initialCommand: command, + aiService: _MockAIRepository(), + ); + }, + act: (bloc) => bloc.init(), + expect: () => [ + isA() + .having((s) => s.markdownText, 'result', isEmpty), + isA() + .having((s) => s.markdownText, 'result', isNotEmpty) + .having((s) => s.markdownText, 'result', contains('UPDATED:')), + isA() + .having((s) => s.markdownText, 'result', isNotEmpty) + .having((s) => s.markdownText, 'result', contains('UPDATED:')), + isA() + .having((s) => s.markdownText, 'result', isNotEmpty) + .having((s) => s.markdownText, 'result', contains('UPDATED:')), + isA() + .having((s) => s.markdownText, 'result', isNotEmpty) + .having((s) => s.markdownText, 'result', contains('UPDATED:')), + ], + ); + + blocTest( + 'exceed the ai response limit', + build: () { + const text1 = '1. Select text to style using the toolbar menu.'; + const text2 = '2. Discover more styling options in Aa.'; + const text3 = + '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + ], + ), + ); + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final node = aiWriterNode( + command: AiWriterCommand.explain, + selection: selection, + ); + return AiWriterCubit( + documentId: '', + getAiWriterNode: () => node, + editorState: editorState, + initialCommand: AiWriterCommand.explain, + aiService: _MockErrorRepository(), + ); + }, + act: (bloc) => bloc.init(), + expect: () => [ + isA() + .having((s) => s.markdownText, 'result', isEmpty), + isA().having( + (s) => s.error.code, + 'error code', + AIErrorCode.aiResponseLimitExceeded, + ), + ], + ); + + test('improve writing - the result contains the same number of paragraphs', + () async { + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + aiWriterNode( + command: AiWriterCommand.improveWriting, + selection: selection, + ), + ], + ), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final aiNode = editorState.getNodeAtPath([3])!; + final bloc = AiWriterCubit( + documentId: '', + getAiWriterNode: () => aiNode, + editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, + aiService: _MockAIRepository(), + ); + bloc.init(); + await blocResponseFuture(); + bloc.runResponseAction(SuggestionAction.accept); + await blocResponseFuture(); + expect( + editorState.document.root.children.length, + 3, + ); + expect( + editorState.getNodeAtPath([0])!.delta!.toPlainText(), + '$_aiResponse $text1', + ); + expect( + editorState.getNodeAtPath([1])!.delta!.toPlainText(), + '$_aiResponse $text2', + ); + expect( + editorState.getNodeAtPath([2])!.delta!.toPlainText(), + '$_aiResponse $text3', + ); + }); + + test('improve writing - discard', () async { + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + aiWriterNode( + command: AiWriterCommand.improveWriting, + selection: selection, + ), + ], + ), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final aiNode = editorState.getNodeAtPath([3])!; + final bloc = AiWriterCubit( + documentId: '', + getAiWriterNode: () => aiNode, + editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, + aiService: _MockAIRepository(), + ); + bloc.init(); + await blocResponseFuture(); + bloc.runResponseAction(SuggestionAction.discard); + await blocResponseFuture(); + expect( + editorState.document.root.children.length, + 3, + ); + expect(editorState.getNodeAtPath([0])!.delta!.toPlainText(), text1); + expect(editorState.getNodeAtPath([1])!.delta!.toPlainText(), text2); + expect(editorState.getNodeAtPath([2])!.delta!.toPlainText(), text3); + }); + + test('improve writing - the result less than the original text', () async { + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + aiWriterNode( + command: AiWriterCommand.improveWriting, + selection: selection, + ), + ], + ), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final aiNode = editorState.getNodeAtPath([3])!; + final bloc = AiWriterCubit( + documentId: '', + getAiWriterNode: () => aiNode, + editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, + aiService: _MockAIRepositoryLess(), + ); + bloc.init(); + await blocResponseFuture(); + bloc.runResponseAction(SuggestionAction.accept); + await blocResponseFuture(); + expect(editorState.document.root.children.length, 1); + expect( + editorState.getNodeAtPath([0])!.delta!.toPlainText(), + 'Hello World', + ); + }); + + test('improve writing - the result more than the original text', () async { + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ); + final document = Document( + root: pageNode( + children: [ + paragraphNode(text: text1), + paragraphNode(text: text2), + paragraphNode(text: text3), + aiWriterNode( + command: AiWriterCommand.improveWriting, + selection: selection, + ), + ], + ), + ); + final editorState = EditorState(document: document) + ..selection = selection; + final aiNode = editorState.getNodeAtPath([3])!; + final bloc = AiWriterCubit( + documentId: '', + getAiWriterNode: () => aiNode, + editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, + aiService: _MockAIRepositoryMore(), + ); + bloc.init(); + await blocResponseFuture(); + bloc.runResponseAction(SuggestionAction.accept); + await blocResponseFuture(); + expect(editorState.document.root.children.length, 10); + for (var i = 0; i < 10; i++) { + expect( + editorState.getNodeAtPath([i])!.delta!.toPlainText(), + 'Hello World', + ); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/ask_ai_test/ask_ai_action_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ask_ai_test/ask_ai_action_bloc_test.dart deleted file mode 100644 index 33e2b4c448..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/ask_ai_test/ask_ai_action_bloc_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:appflowy/ai/service/ai_client.dart'; -import 'package:appflowy/ai/service/appflowy_ai_service.dart'; -import 'package:appflowy/ai/service/error.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ask_ai_action_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../util.dart'; - -const _aiResponse = 'UPDATED:'; - -class _MockAIRepository extends Mock implements AIRepository { - @override - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }) async { - await onStart(); - final lines = text.split('\n\n'); - for (var i = 0; i < lines.length; i++) { - await onProcess('$_aiResponse ${lines[i]}\n\n'); - } - await onEnd(); - } -} - -class _MockAIRepositoryLess extends Mock implements AIRepository { - @override - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }) async { - await onStart(); - // only return 1 line. - await onProcess('Hello World'); - await onEnd(); - } -} - -class _MockAIRepositoryMore extends Mock implements AIRepository { - @override - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }) async { - await onStart(); - // return 10 lines - for (var i = 0; i < 10; i++) { - await onProcess('Hello World\n\n'); - } - await onEnd(); - } -} - -class _MockErrorRepository extends Mock implements AIRepository { - @override - Future streamCompletion({ - String? objectId, - required String text, - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) onProcess, - required Future Function() onEnd, - required void Function(AIError error) onError, - }) async { - await onStart(); - onError( - const AIError( - message: 'Error', - code: AIErrorCode.aiResponseLimitExceeded, - ), - ); - } -} - -void main() { - group('AskAIActionBloc: ', () { - const text1 = '1. Select text to style using the toolbar menu.'; - const text2 = '2. Discover more styling options in Aa.'; - const text3 = - '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; - - blocTest( - 'send request before the bloc is initialized', - build: () { - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final editorState = EditorState(document: document); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - - final node = askAINode( - action: AskAIAction.makeItLonger, - content: [text1, text2, text3].join('\n'), - ); - return AskAIActionBloc( - objectId: "", - node: node, - editorState: editorState, - action: AskAIAction.makeItLonger, - enableLogging: false, - ); - }, - act: (bloc) { - bloc.add(AskAIEvent.initial(Future.value(_MockAIRepository()))); - bloc.add(const AskAIEvent.rewrite()); - }, - expect: () => [ - isA() - .having((s) => s.loading, 'loading', true) - .having((s) => s.result, 'result', isEmpty), - isA() - .having((s) => s.loading, 'loading', false) - .having((s) => s.result, 'result', isNotEmpty) - .having((s) => s.result, 'result', contains('UPDATED:')), - isA().having((s) => s.loading, 'loading', false), - ], - ); - - blocTest( - 'exceed the ai response limit', - build: () { - const text1 = '1. Select text to style using the toolbar menu.'; - const text2 = '2. Discover more styling options in Aa.'; - const text3 = - '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final editorState = EditorState(document: document); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - - final node = askAINode( - action: AskAIAction.makeItLonger, - content: [text1, text2, text3].join('\n'), - ); - return AskAIActionBloc( - objectId: "", - node: node, - editorState: editorState, - action: AskAIAction.makeItLonger, - enableLogging: false, - ); - }, - act: (bloc) { - bloc.add(AskAIEvent.initial(Future.value(_MockErrorRepository()))); - bloc.add(const AskAIEvent.rewrite()); - }, - expect: () => [ - isA() - .having((s) => s.loading, 'loading', true) - .having((s) => s.result, 'result', isEmpty), - isA() - .having((s) => s.requestError, 'requestError', isNotNull) - .having( - (s) => s.requestError?.code, - 'requestError.code', - AIErrorCode.aiResponseLimitExceeded, - ), - ], - ); - - test('summary - the result contains the same number of paragraphs', - () async { - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final editorState = EditorState(document: document); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - - final node = askAINode( - action: AskAIAction.makeItLonger, - content: [text1, text2, text3].join('\n\n'), - ); - final bloc = AskAIActionBloc( - objectId: "", - node: node, - editorState: editorState, - action: AskAIAction.summarize, - enableLogging: false, - ); - bloc.add(AskAIEvent.initial(Future.value(_MockAIRepository()))); - await blocResponseFuture(); - bloc.add(const AskAIEvent.started()); - await blocResponseFuture(); - bloc.add(const AskAIEvent.replace()); - await blocResponseFuture(); - expect(editorState.document.root.children.length, 3); - expect( - editorState.getNodeAtPath([0])!.delta!.toPlainText(), - '$_aiResponse $text1', - ); - expect( - editorState.getNodeAtPath([1])!.delta!.toPlainText(), - '$_aiResponse $text2', - ); - expect( - editorState.getNodeAtPath([2])!.delta!.toPlainText(), - '$_aiResponse $text3', - ); - }); - - test('summary - the result less than the original text', () async { - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final editorState = EditorState(document: document); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - - final node = askAINode( - action: AskAIAction.makeItLonger, - content: [text1, text2, text3].join('\n'), - ); - final bloc = AskAIActionBloc( - objectId: "", - node: node, - editorState: editorState, - action: AskAIAction.summarize, - enableLogging: false, - ); - bloc.add(AskAIEvent.initial(Future.value(_MockAIRepositoryLess()))); - await blocResponseFuture(); - bloc.add(const AskAIEvent.started()); - await blocResponseFuture(); - bloc.add(const AskAIEvent.replace()); - await blocResponseFuture(); - expect(editorState.document.root.children.length, 1); - expect( - editorState.getNodeAtPath([0])!.delta!.toPlainText(), - 'Hello World', - ); - }); - - test('summary - the result more than the original text', () async { - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final editorState = EditorState(document: document); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - - final node = askAINode( - action: AskAIAction.makeItLonger, - content: [text1, text2, text3].join('\n'), - ); - final bloc = AskAIActionBloc( - objectId: "", - node: node, - editorState: editorState, - action: AskAIAction.summarize, - enableLogging: false, - ); - bloc.add(AskAIEvent.initial(Future.value(_MockAIRepositoryMore()))); - await blocResponseFuture(); - bloc.add(const AskAIEvent.started()); - await blocResponseFuture(); - bloc.add(const AskAIEvent.replace()); - await blocResponseFuture(); - expect(editorState.document.root.children.length, 10); - for (var i = 0; i < 10; i++) { - expect( - editorState.getNodeAtPath([i])!.delta!.toPlainText(), - 'Hello World', - ); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart index 29d98416b5..ece0c5e027 100644 --- a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart @@ -34,11 +34,3 @@ class AppFlowyChatTest { }); } } - -Future boardResponseFuture() { - return Future.delayed(boardResponseDuration()); -} - -Duration boardResponseDuration({int milliseconds = 200}) { - return Duration(milliseconds: milliseconds); -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart index ed60e53ae7..0b6289c784 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart @@ -29,7 +29,7 @@ void main() { // mock the delay of the text robot await Future.delayed(const Duration(milliseconds: 10)); } - await markdownTextRobot.stop(); + await markdownTextRobot.persist(); expect(editorState); } @@ -43,11 +43,10 @@ void main() { markdownTextRobot.start(); await markdownTextRobot.appendMarkdownText(_sample1); - await markdownTextRobot.stop(); + await markdownTextRobot.persist(); final nodes = editorState.document.root.children; - // 4 from the sample, 1 from the original empty paragraph node - expect(nodes.length, 5); + expect(nodes.length, 4); final n1 = nodes[0]; expect(n1.delta!.toPlainText(), 'The Curious Cat'); @@ -116,7 +115,7 @@ void main() { _liveRefreshSample2, expect: (editorState) { final nodes = editorState.document.root.children; - expect(nodes.length, 5); + expect(nodes.length, 4); final n1 = nodes[0]; expect(n1.type, HeadingBlockKeys.type); @@ -164,7 +163,7 @@ void main() { _liveRefreshSample3, expect: (editorState) { final nodes = editorState.document.root.children; - expect(nodes.length, 6); + expect(nodes.length, 5); final n1 = nodes[0]; expect(n1.type, HeadingBlockKeys.type); @@ -275,7 +274,7 @@ void main() { _liveRefreshSample4, expect: (editorState) { final nodes = editorState.document.root.children; - expect(nodes.length, 3); + expect(nodes.length, 2); final n1 = nodes[0]; expect(n1.type, ParagraphBlockKeys.type); diff --git a/frontend/resources/flowy_icons/16x/ai_explain.svg b/frontend/resources/flowy_icons/16x/ai_explain.svg new file mode 100644 index 0000000000..173b35b543 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_explain.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg b/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg new file mode 100644 index 0000000000..2f7df6c78a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_improve_writing.svg b/frontend/resources/flowy_icons/16x/ai_improve_writing.svg new file mode 100644 index 0000000000..9cff9e9875 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_improve_writing.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/ai_make_longer.svg b/frontend/resources/flowy_icons/16x/ai_make_longer.svg new file mode 100644 index 0000000000..9f61441f0f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_make_longer.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_make_shorter.svg b/frontend/resources/flowy_icons/16x/ai_make_shorter.svg new file mode 100644 index 0000000000..5f07c58fcc --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_make_shorter.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_sparks.svg b/frontend/resources/flowy_icons/16x/ai_sparks.svg index dc23a6c287..a419454f8e 100644 --- a/frontend/resources/flowy_icons/16x/ai_sparks.svg +++ b/frontend/resources/flowy_icons/16x/ai_sparks.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/resources/flowy_icons/16x/ai_summarize.svg b/frontend/resources/flowy_icons/16x/ai_summarize.svg new file mode 100644 index 0000000000..761dae9dc0 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_summarize.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_undo.svg b/frontend/resources/flowy_icons/16x/ai_try_again.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/ai_undo.svg rename to frontend/resources/flowy_icons/16x/ai_try_again.svg diff --git a/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg b/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg new file mode 100644 index 0000000000..aa071665c3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 1326fabad1..23b9d4abdc 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -387,7 +387,6 @@ "storageLimitDialogTitle": "لقد نفدت مساحة التخزين المجانية لديك. قم بالترقية لإلغاء تأمين مساحة تخزين غير محدودة", "storageLimitDialogTitleIOS": "لقد نفدت مساحة التخزين المجانية.", "aiResponseLimitTitle": "لقد نفدت منك استجابات الذكاء الاصطناعي المجانية. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "aiResponseLimitTitleIOS": "لقد نفدت استجابات الذكاء الاصطناعي المجانية.", "aiResponseLimitDialogTitle": "تم الوصول إلى الحد الأقصى لاستجابات الذكاء الاصطناعي", "aiResponseLimit": "لقد نفدت استجابات الذكاء الاصطناعي المجانية.\nانتقل إلى الإعدادات -> الخطة -> انقر فوق AI Max أو Pro Plan للحصول على المزيد من استجابات الذكاء الاصطناعي", "askOwnerToUpgradeToPro": "مساحة العمل الخاصة بك نفدت من مساحة التخزين المجانية. يرجى مطالبة مالك مساحة العمل الخاصة بك بالترقية إلى الخطة الاحترافية", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index f750139742..996ed03b7d 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -338,7 +338,6 @@ "storageLimitDialogTitle": "Dein freier Speicherplatz ist aufgebraucht. Upgrade deinen Plan, um unbegrenzten Speicherplatz freizuschalten.", "storageLimitDialogTitleIOS": "Ihr freier Speicherplatz ist aufgebraucht.", "aiResponseLimitTitle": "Du hast keine kostenlosen KI-Antworten mehr. Upgrade auf den Pro-Plan oder kaufe ein KI-Add-on, um unbegrenzte Antworten freizuschalten", - "aiResponseLimitTitleIOS": "Ihre kostenlosen KI-Antworten sind aufgebraucht.", "aiResponseLimitDialogTitle": "Limit für KI-Antworten erreicht", "aiResponseLimit": "Du hast keine kostenlosen KI-Antworten mehr zur Verfügung.\n\nGehe zu Einstellungen -> Plan -> Klicke auf KI Max oder Pro Plan, um mehr KI-Antworten zu erhalten", "askOwnerToUpgradeToPro": "Dein Arbeitsbereich hat nicht mehr genügend freien Speicherplatz. Bitte den Eigentümer deines Arbeitsbereichs, auf den Pro-Plan hochzustufen.", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b64fe35436..d2c9eaaf23 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -214,7 +214,7 @@ "indexFileSuccess": "Indexing file successfully", "inputActionNoPages": "No page results", "referenceSource": { - "zero": "0 sources gefunden", + "zero": "0 sources found", "one": "{count} source found", "other": "{count} sources found" }, @@ -224,6 +224,7 @@ "indexingFile": "Indexing {}", "generatingResponse": "Generating response", "selectSources": "Select Sources", + "currentPage": "Current page", "sourcesLimitReached": "You can only select up to 3 top-level documents and its children", "sourceUnsupported": "We don't support chatting with databases at this time", "regenerate": "Try again", @@ -383,7 +384,6 @@ "storageLimitDialogTitle": "You have run out of free storage. Upgrade to unlock unlimited storage", "storageLimitDialogTitleIOS": "You have run out of free storage.", "aiResponseLimitTitle": "You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", - "aiResponseLimitTitleIOS": "You have run out of free AI responses.", "aiResponseLimitDialogTitle": "AI Responses limit reached", "aiResponseLimit": "You have run out of free AI responses.\n\nGo to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses", "askOwnerToUpgradeToPro": "Your workspace is running out of free storage. Please ask your workspace owner to upgrade to the Pro Plan", @@ -1733,7 +1733,7 @@ "toggleHeading2": "Toggle heading 2", "toggleHeading3": "Toggle heading 3", "emoji": "Emoji", - "aiWriter": "AI Writer", + "aiWriter": "Ask AI Anything", "dateOrReminder": "Date or Reminder", "photoGallery": "Photo Gallery", "file": "File", @@ -1762,6 +1762,16 @@ "referencedGrid": "Referenced Grid", "referencedCalendar": "Referenced Calendar", "referencedDocument": "Referenced Document", + "aiWriter": { + "userQuestion": "Ask AI anything", + "continueWriting": "Continue writing", + "fixSpelling": "Fix spelling & grammar", + "improveWriting": "Improve writing", + "summarize": "Summarize", + "explain": "Explain", + "makeShorter": "Make shorter", + "makeLonger": "Make longer" + }, "autoGeneratorMenuItemName": "AI Writer", "autoGeneratorTitleName": "AI: Ask AI to write anything...", "autoGeneratorLearnMore": "Learn more", @@ -1780,7 +1790,7 @@ "smartEditCouldNotFetchKey": "Could not fetch AI key", "smartEditDisabled": "Connect AI in Settings", "appflowyAIEditDisabled": "Sign in to enable AI features", - "discardResponse": "Do you want to discard the AI responses?", + "discardResponse": "Are you sure you want to discard the AI response?", "createInlineMathEquation": "Create equation", "fonts": "Fonts", "insertDate": "Insert date", @@ -3108,7 +3118,22 @@ } }, "ai": { - "contentPolicyViolation": "Image generation failed due to sensitive content. Please rephrase your input and try again" + "contentPolicyViolation": "Image generation failed due to sensitive content. Please rephrase your input and try again", + "textLimitReachedDescription": "Your workspace has run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", + "imageLimitReachedDescription": "You've used up your free AI image quota. Please upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", + "limitReachedAction": { + "textDescription": "Your workspace has run out of free AI responses. To get more responses, please", + "imageDescription": "You've used up your free AI image quota. Please", + "upgrade": "upgrade", + "toThe": "to the", + "proPlan": "Pro Plan", + "orPurchaseAn": "or purchase an", + "aiAddon": "AI add-on" + }, + "editing": "Editing", + "analyzing": "Analyzing", + "continueWritingEmptyDocumentTitle": "Continue writing error", + "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!" }, "autoUpdate": { "criticalUpdateTitle": "Update required to continue", @@ -3128,5 +3153,14 @@ "lockTooltip": "Page locked to prevent accidental editing. Click to unlock.", "pageLockedToast": "Page locked. Editing is disabled until someone unlocks it.", "lockedOperationTooltip": "Page locked to prevent accidental editing." + }, + "suggestion": { + "accept": "Accept", + "keep": "Keep", + "discard": "Discard", + "close": "Close", + "tryAgain": "Try again", + "rewrite": "Rewrite", + "insertBelow": "Insert below" } } diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 326d2d044f..270b92d72f 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -354,7 +354,6 @@ "storageLimitDialogTitle": "Vous n'avez plus d'espace de stockage gratuit. Effectuez une mise à niveau pour débloquer un espace de stockage illimité", "storageLimitDialogTitleIOS": "Vous n'avez plus d'espace de stockage gratuit.", "aiResponseLimitTitle": "Vous n'avez plus de réponses d'IA gratuites. Passez au plan Pro ou achetez un module complémentaire d'IA pour débloquer des réponses illimitées", - "aiResponseLimitTitleIOS": "Vous n'avez plus de réponses IA gratuites.", "aiResponseLimitDialogTitle": "La limite des réponses de l'IA a été atteinte", "aiResponseLimit": "Vous n'avez plus de réponses IA gratuites.\n\nAccédez à Paramètres -> Plans -> Cliquez sur AI Max ou Pro Plan pour obtenir plus de réponses AI", "askOwnerToUpgradeToPro": "Votre espace de stockage gratuit est presque plein. Demandez au propriétaire de votre espace de travail de passer au plan Pro", diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index cf33c91e88..cd41f16530 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -346,9 +346,7 @@ "upgradeToPro": "Proプランにアップグレード", "upgradeToAIMax": "無制限のAIを解放", "storageLimitDialogTitle": "無料のストレージが不足しています。無制限のストレージを解放するにはアップグレードしてください", - "storageLimitDialogTitleIOS": "無料のストレージが不足しています。", "aiResponseLimitTitle": "無料のAIレスポンスが不足しています。Proプランにアップグレードするか、AIアドオンを購入して無制限のレスポンスを解放してください", - "aiResponseLimitTitleIOS": "無料のAIレスポンスが不足しています。", "aiResponseLimitDialogTitle": "AIレスポンスの制限に達しました", "aiResponseLimit": "無料のAIレスポンスが不足しています。\n\n設定 -> プラン -> AI MaxまたはProプランをクリックして、さらにAIレスポンスを取得してください", "askOwnerToUpgradeToPro": "ワークスペースの無料ストレージが不足しています。ワークスペースのオーナーにProプランへのアップグレードを依頼してください", diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 6bfa0c0ea0..0e888fdb9b 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -333,7 +333,6 @@ "storageLimitDialogTitle": "คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว กรุณาอัปเกรดเพื่อปลดล็อกพื้นที่จัดเก็บแบบไม่จำกัด", "storageLimitDialogTitleIOS": "คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว", "aiResponseLimitTitle": "คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว กรุณาอัปเกรดเป็นแผน Pro หรือซื้อส่วนเสริม AI เพื่อปลดล็อกการตอบกลับไม่จำกัด", - "aiResponseLimitTitleIOS": "คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว", "aiResponseLimitDialogTitle": "ถึงขีดจำกัดการตอบกลับ AI แล้ว", "aiResponseLimit": "คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว\nไปที่ การตั้งค่า -> แผน -> คลิก AI Max หรือแผน Pro เพื่อรับการตอบกลับ AI เพิ่มเติม", "askOwnerToUpgradeToPro": "พื้นที่จัดเก็บฟรีในพื้นที่ทำงานของคุณกำลังจะหมด กรุณาขอให้เจ้าของพื้นที่ทำงานของคุณอัปเกรดเป็นแผน Pro", diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index d2d0c9eea6..d23566e512 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -373,7 +373,6 @@ "storageLimitDialogTitle": "Ücretsiz depolama alanınız bitti. Sınırsız depolama için yükseltin", "storageLimitDialogTitleIOS": "Ücretsiz depolama alanınız bitti.", "aiResponseLimitTitle": "Ücretsiz yapay zeka yanıtlarınız bitti. Sınırsız yanıt için Pro Plana yükseltin veya bir yapay zeka eklentisi satın alın", - "aiResponseLimitTitleIOS": "Ücretsiz yapay zeka yanıtlarınız bitti.", "aiResponseLimitDialogTitle": "Yapay zeka yanıt limitine ulaşıldı", "aiResponseLimit": "Ücretsiz yapay zeka yanıtlarınız bitti.\n\nDaha fazla yapay zeka yanıtı almak için Ayarlar -> Plan -> AI Max veya Pro Plan'a tıklayın", "askOwnerToUpgradeToPro": "Çalışma alanınızın ücretsiz depolama alanı bitiyor. Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 91ea6a32e8..a506a84db3 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -308,7 +308,6 @@ "storageLimitDialogTitle": "Bạn đã hết dung lượng lưu trữ miễn phí. Nâng cấp để mở khóa dung lượng lưu trữ không giới hạn", "storageLimitDialogTitleIOS": "Bạn đã hết dung lượng lưu trữ miễn phí.", "aiResponseLimitTitle": "Bạn đã hết phản hồi AI miễn phí. Nâng cấp lên Gói Pro hoặc mua tiện ích bổ sung AI để mở khóa phản hồi không giới hạn", - "aiResponseLimitTitleIOS": "Bạn đã hết lượt sử dụng AI miễn phí.", "aiResponseLimitDialogTitle": "Đã đạt đến giới hạn sử dụng AI", "aiResponseLimit": "Bạn đã hết lượt dùng AI miễn phí.\nVào Cài đặt -> Gói đăng ký -> Nhấp vào AI Max hoặc Gói Pro để có thêm lượt dùng AI", "askOwnerToUpgradeToPro": "Không gian làm việc của bạn sắp hết dung lượng lưu trữ miễn phí. Vui lòng yêu cầu chủ sở hữu không gian làm việc của bạn nâng cấp lên Gói Pro", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 5cac16a886..ce0c760f3d 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -356,7 +356,6 @@ "storageLimitDialogTitle": "你已用尽免费存储。升级以解锁无限制存储", "storageLimitDialogTitleIOS": "你已用尽免费存储。", "aiResponseLimitTitle": "你已用尽免费 AI 回应。升级到专业版或者购买 AI 插件来解锁无限制回应", - "aiResponseLimitTitleIOS": "你已用尽免费 AI 回应。", "aiResponseLimitDialogTitle": "已达到 AI 回应限额", "aiResponseLimit": "你已用尽了免费 AI 回应。\n\n转到“设置 -> 计划 -> 点击 AI Max 或 Pro 计划”获取更多 AI 回应", "askOwnerToUpgradeToPro": "你的工作区即将用尽免费存储。请联系工作区所有者升级到专业版计划", diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 34f86cd383..fe66a58aa8 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -297,7 +297,6 @@ "storageLimitDialogTitle": "您的免費儲存空間已用完,升級以解鎖無限儲存空間", "storageLimitDialogTitleIOS": "您的免費儲存空間已用完。", "aiResponseLimitTitle": "您的免費 AI 回覆已用完,升級到 Pro 或購買 AI 附加方案以解鎖無限回覆", - "aiResponseLimitTitleIOS": "您的免費 AI 回覆已用完。", "aiResponseLimitDialogTitle": "AI 回覆已達到限制", "purchaseStorageSpace": "購買儲存空間", "purchaseAIResponse": "購買" diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index f1aafcd136..c5ac604397 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -99,6 +99,7 @@ impl EventIntegrationTest { stream_port: 0, object_id: "".to_string(), rag_ids: vec![], + format: None, }; EventBuilder::new(self.clone()) .event(AIEvent::CompleteText) diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 7ce7f79a2e..28c64d8a8f 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -88,13 +88,13 @@ impl CompletionTask { if let Some(cloud_service) = self.cloud_service.upgrade() { let complete_type = match self.context.completion_type { - CompletionTypePB::UnknownCompletionType | CompletionTypePB::ImproveWriting => { - CompletionType::ImproveWriting - }, + CompletionTypePB::ImproveWriting => CompletionType::ImproveWriting, CompletionTypePB::SpellingAndGrammar => CompletionType::SpellingAndGrammar, CompletionTypePB::MakeShorter => CompletionType::MakeShorter, CompletionTypePB::MakeLonger => CompletionType::MakeLonger, CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, + CompletionTypePB::ExplainSelected => CompletionType::Explain, + _ => CompletionType::ContinueWriting, }; let _ = sink.send("start:".to_string()).await; @@ -107,7 +107,7 @@ impl CompletionTask { workspace_id: None, rag_ids: Some(self.context.rag_ids), }), - format: Default::default(), + format: self.context.format.map(Into::into).unwrap_or_default(), }; info!("start completion: {:?}", params); @@ -146,6 +146,7 @@ impl CompletionTask { }); } } + async fn handle_error(sink: &mut IsolateSink, error: FlowyError) { if error.is_ai_response_limit_exceeded() { let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await; diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 6a30a7dff7..7b1cb66593 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -365,13 +365,16 @@ pub struct CompleteTextPB { #[pb(index = 2)] pub completion_type: CompletionTypePB, - #[pb(index = 3)] - pub stream_port: i64, + #[pb(index = 3, one_of)] + pub format: Option, #[pb(index = 4)] - pub object_id: String, + pub stream_port: i64, #[pb(index = 5)] + pub object_id: String, + + #[pb(index = 6)] pub rag_ids: Vec, } @@ -383,13 +386,14 @@ pub struct CompleteTextTaskPB { #[derive(Clone, Debug, ProtoBuf_Enum, Default)] pub enum CompletionTypePB { - UnknownCompletionType = 0, #[default] - ImproveWriting = 1, - SpellingAndGrammar = 2, - MakeShorter = 3, - MakeLonger = 4, - ContinueWriting = 5, + UserQuestion = 0, + ExplainSelected = 1, + ContinueWriting = 2, + SpellingAndGrammar = 3, + ImproveWriting = 4, + MakeShorter = 5, + MakeLonger = 6, } #[derive(Default, ProtoBuf, Clone, Debug)] From 655de30df5bb945df35c4a0113e4b7208cad909c Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Mar 2025 16:40:41 +0800 Subject: [PATCH 061/384] fix: unable to delete the callout block when it's in the first line (#7442) --- .../presentation/editor_configuration.dart | 18 ++++++++++++++++-- .../document/presentation/editor_page.dart | 5 ++++- .../callout/callout_block_component.dart | 3 +++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index d5e44301ad..20049236ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -700,7 +700,14 @@ TableBlockComponentBuilder _buildTableBlockComponentBuilder( BlockComponentConfiguration configuration, ) { return TableBlockComponentBuilder( - menuBuilder: (node, editorState, position, dir, onBuild, onClose) => + menuBuilder: ( + node, + editorState, + position, + dir, + onBuild, + onClose, + ) => TableMenu( node: node, editorState: editorState, @@ -727,7 +734,14 @@ TableCellBlockComponentBuilder _buildTableCellBlockComponentBuilder( } return buildEditorCustomizedColor(context, node, colorString); }, - menuBuilder: (node, editorState, position, dir, onBuild, onClose) => + menuBuilder: ( + node, + editorState, + position, + dir, + onBuild, + onClose, + ) => TableMenu( node: node, editorState: editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 9d1119ecf1..7d9d2eb790 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -154,7 +154,10 @@ class _AppFlowyEditorPageState extends State ]); indentableBlockTypes.add(ToggleListBlockKeys.type); - convertibleBlockTypes.add(ToggleListBlockKeys.type); + convertibleBlockTypes.addAll([ + ToggleListBlockKeys.type, + CalloutBlockKeys.type, + ]); effectiveScrollController = widget.scrollController ?? ScrollController(); // disable the color parse in the HTML decoder. diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index c81c2f3be6..1d9f70254f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -210,6 +210,9 @@ class _CalloutBlockComponentWidgetState key: ValueKey(widget.node.id + emoji.emoji), enable: editorState.editable, title: '', + margin: UniversalPlatform.isMobile + ? const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0) + : EdgeInsets.zero, emoji: emoji, emojiSize: emojiSize, showBorder: false, From aff720c1f17476130ed2180d893c5f1affaab5ba Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Mar 2025 11:29:24 +0800 Subject: [PATCH 062/384] fix: hide the improve writing button (#7443) * fix: hide the improve writing button * chore: update appflowy_editor version * fix: simple column width * Revert "fix: hide the improve writing button" This reverts commit 815a28971c3d1352ce89f8888ff0956cc48aced0. --- .../lib/plugins/document/presentation/editor_page.dart | 10 ++++++++++ .../editor_plugins/actions/drag_to_reorder/util.dart | 10 ++++++++-- .../slash_menu_items/simple_columns_item.dart | 2 +- frontend/appflowy_flutter/pubspec.lock | 6 +++--- frontend/appflowy_flutter/pubspec.yaml | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 7d9d2eb790..20bba4f083 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,5 +1,6 @@ import 'dart:ui' as ui; +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart'; @@ -159,6 +160,15 @@ class _AppFlowyEditorPageState extends State CalloutBlockKeys.type, ]); + editorLaunchUrl = (url) { + if (url != null) { + afLaunchUrlString(url); + } + + return Future.value(true); + }; + + effectiveScrollController = widget.scrollController ?? ScrollController(); // disable the color parse in the HTML decoder. DocumentHTMLDecoder.enableColorParse = false; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index 99b13add54..bf9b1bdc7a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -81,17 +81,23 @@ Future dragToMoveNode( final transaction = editorState.transaction; final targetNodeParent = targetNode.parentColumnsBlock; if (targetNodeParent != null) { + // find the previous sibling node of the target node + final width = (node.rect.width / + (targetNode.parentColumnsBlock?.children.length ?? 2)) - + 16; final columnNode = simpleColumnNode( children: [node.deepCopy()], + width: width, ); transaction.insertNode(targetNode.path.previous, columnNode); transaction.deleteNode(node); } else { + final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [node.deepCopy()]), - simpleColumnNode(children: [targetNode.deepCopy()]), + simpleColumnNode(children: [node.deepCopy()], width: width), + simpleColumnNode(children: [targetNode.deepCopy()], width: width), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart index 103cfface4..a0966465a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart @@ -95,7 +95,7 @@ Node _buildColumnsNode(EditorState editorState, int columnCount) { if (selection != null) { final parentNode = editorState.getNodeAtPath(selection.start.path); if (parentNode != null) { - width = parentNode.rect.width / columnCount; + width = parentNode.rect.width / columnCount - 16; } } return simpleColumnsNode(columnCount: columnCount, width: width); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 3174f2f1c5..fad0defff3 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,11 +90,11 @@ packages: dependency: "direct main" description: path: "." - ref: "400083f" - resolved-ref: "400083fde6a1a77229416a55c50279f5f5b3f55b" + ref: b2672f2 + resolved-ref: b2672f297b0e9e7a028ea217ae6a57c8dca3da4e url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "5.0.0" + version: "5.1.0" appflowy_editor_plugins: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 111eac6e31..a93b261419 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -179,7 +179,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "400083f" + ref: "b2672f2" appflowy_editor_plugins: git: From 9eed9934213d6e2c7d32bedd0407977653d0b312 Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 4 Mar 2025 11:29:38 +0800 Subject: [PATCH 063/384] feat: refactor databse styles (#7405) * feat: refactor databse styles * feat: support compact mode for databse * feat: support dynamic height for board * fix: add reference icon for database view in document * feat: support data sync for database node in document * fix: add hover effect in compact mode switcher * fix: title of document not align correctly with a large screen * fix: some launch review issues * fix: auto hide the Hidden Groups unless the user clicks it to show * fix: testing error * chore: update board version * chore: update database menu buttons * fix: some launch review issues --------- Co-authored-by: Lucas --- .../desktop/board/board_hide_groups_test.dart | 23 +-- .../shared/common_operations.dart | 2 +- .../lib/core/config/kv_keys.dart | 5 + .../mobile_card_detail_screen.dart | 8 +- .../application/database_controller.dart | 40 ++++- .../application/layout/layout_bloc.dart | 1 + .../database/application/tab_bar_bloc.dart | 60 ++++++- .../board/presentation/board_page.dart | 164 ++++++++++-------- .../widgets/board_hidden_groups.dart | 24 ++- .../presentation/calendar_event_editor.dart | 1 + .../database/grid/presentation/grid_page.dart | 35 +++- .../grid/presentation/layout/layout.dart | 1 + .../grid/presentation/layout/sizes.dart | 18 +- .../widgets/header/grid_header.dart | 16 +- .../grid/presentation/widgets/row/row.dart | 22 ++- .../widgets/toolbar/filter_button.dart | 41 ++--- .../widgets/toolbar/grid_setting_bar.dart | 11 +- .../widgets/toolbar/sort_button.dart | 39 ++--- .../widgets/toolbar/view_database_button.dart | 37 ++++ .../tab_bar/desktop/tab_bar_header.dart | 28 ++- .../database/tab_bar/tab_bar_view.dart | 160 ++++++++++++++++- .../plugins/database/widgets/card/card.dart | 6 +- .../card/container/card_container.dart | 6 +- .../desktop_grid_checkbox_cell.dart | 37 ++-- .../desktop_grid_checklist_cell.dart | 31 ++-- .../desktop_grid/desktop_grid_date_cell.dart | 62 ++++--- .../desktop_grid/desktop_grid_media_cell.dart | 4 +- .../desktop_grid_number_cell.dart | 46 +++-- .../desktop_grid_relation_cell.dart | 18 +- .../desktop_grid_select_option_cell.dart | 112 +++++++----- .../desktop_grid_summary_cell.dart | 114 ++++++------ .../desktop_grid/desktop_grid_text_cell.dart | 69 ++++---- .../desktop_grid_timestamp_cell.dart | 34 ++-- .../desktop_grid_translate_cell.dart | 134 +++++++------- .../desktop_grid/desktop_grid_url_cell.dart | 51 +++--- .../desktop_row_detail_checkbox_cell.dart | 3 +- .../desktop_row_detail_checklist_cell.dart | 1 + .../desktop_row_detail_date_cell.dart | 5 +- .../desktop_row_detail_number_cell.dart | 3 +- .../desktop_row_detail_relation_cell.dart | 1 + ...desktop_row_detail_select_option_cell.dart | 4 +- .../desktop_row_detail_summary_cell.dart | 1 + .../desktop_row_detail_text_cell.dart | 3 +- .../desktop_row_detail_timestamp_cell.dart | 3 +- .../desktop_row_detail_url_cell.dart | 3 +- .../destop_row_detail_translate_cell.dart | 1 + .../cell/editable_cell_skeleton/checkbox.dart | 6 +- .../editable_cell_skeleton/checklist.dart | 6 +- .../cell/editable_cell_skeleton/date.dart | 6 +- .../cell/editable_cell_skeleton/number.dart | 6 +- .../cell/editable_cell_skeleton/relation.dart | 6 +- .../editable_cell_skeleton/select_option.dart | 6 +- .../cell/editable_cell_skeleton/summary.dart | 7 +- .../cell/editable_cell_skeleton/text.dart | 6 +- .../editable_cell_skeleton/timestamp.dart | 6 +- .../editable_cell_skeleton/translate.dart | 7 +- .../cell/editable_cell_skeleton/url.dart | 6 +- .../mobile_grid_checkbox_cell.dart | 3 +- .../mobile_grid_checklist_cell.dart | 5 +- .../mobile_grid/mobile_grid_date_cell.dart | 6 +- .../mobile_grid/mobile_grid_number_cell.dart | 3 +- .../mobile_grid_relation_cell.dart | 3 +- .../mobile_grid_select_option_cell.dart | 5 +- .../mobile_grid/mobile_grid_summary_cell.dart | 1 + .../mobile_grid/mobile_grid_text_cell.dart | 1 + .../mobile_grid_timestamp_cell.dart | 3 +- .../mobile_grid_translate_cell.dart | 1 + .../mobile_grid/mobile_grid_url_cell.dart | 1 + .../mobile_row_detail_checkbox_cell.dart | 4 +- .../mobile_row_detail_checklist_cell.dart | 1 + .../mobile_row_detail_date_cell.dart | 3 +- .../mobile_row_detail_number_cell.dart | 3 +- .../mobile_row_detail_relation_cell.dart | 3 +- .../mobile_row_detail_select_cell_option.dart | 5 +- .../mobile_row_detail_summary_cell.dart | 1 + .../mobile_row_detail_text_cell.dart | 3 +- .../mobile_row_detail_timestamp_cell.dart | 3 +- .../mobile_row_detail_translate_cell.dart | 1 + .../mobile_row_detail_url_cell.dart | 3 +- .../widgets/database_view_widget.dart | 24 ++- .../widgets/group/database_group.dart | 41 +++-- .../widgets/row/cells/cell_container.dart | 3 +- .../database/widgets/row/row_banner.dart | 1 + .../database/widgets/row/row_detail.dart | 70 ++++---- .../database/widgets/row/row_document.dart | 35 ++-- .../setting/database_layout_selector.dart | 80 ++++++--- .../setting/database_setting_action.dart | 5 +- .../widgets/setting/setting_button.dart | 26 +-- .../presentation/database_document_title.dart | 1 + .../presentation/compact_mode_event.dart | 13 ++ .../presentation/editor_configuration.dart | 7 +- .../actions/block_action_list.dart | 2 +- .../base/built_in_page_widget.dart | 98 +---------- .../database_view_block_component.dart | 77 +++++--- .../header/document_cover_widget.dart | 1 + .../mention/mention_page_block.dart | 6 + .../custom_page_block_component.dart | 116 +++++++++++++ .../toggle/toggle_block_component.dart | 19 +- .../plugins/shared/share/share_button.dart | 7 +- .../workspace/application/view/view_ext.dart | 1 + frontend/appflowy_flutter/pubspec.lock | 12 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- .../flowy_icons/16x/database_filter.svg | 5 + .../flowy_icons/16x/database_fullscreen.svg | 4 + .../16x/database_settings_arrow_right.svg | 3 + .../flowy_icons/16x/database_sort.svg | 4 + frontend/resources/translations/en.json | 1 + .../src/services/setting/entities.rs | 5 +- 108 files changed, 1522 insertions(+), 766 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart create mode 100644 frontend/resources/flowy_icons/16x/database_filter.svg create mode 100644 frontend/resources/flowy_icons/16x/database_fullscreen.svg create mode 100644 frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg create mode 100644 frontend/resources/flowy_icons/16x/database_sort.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart index 15da47f0f1..6a012ac763 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart @@ -23,24 +23,24 @@ void main() { final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s); // Is expanded by default - expect(collapseFinder, findsOneWidget); - expect(expandFinder, findsNothing); - - // Collapse hidden groups - await tester.tap(collapseFinder); - await tester.pumpAndSettle(); - - // Is collapsed expect(collapseFinder, findsNothing); expect(expandFinder, findsOneWidget); - // Expand hidden groups + // Collapse hidden groups await tester.tap(expandFinder); await tester.pumpAndSettle(); - // Is expanded + // Is collapsed expect(collapseFinder, findsOneWidget); expect(expandFinder, findsNothing); + + // Expand hidden groups + await tester.tap(collapseFinder); + await tester.pumpAndSettle(); + + // Is expanded + expect(collapseFinder, findsNothing); + expect(expandFinder, findsOneWidget); }); testWidgets('hide first group, and show it again', (tester) async { @@ -48,6 +48,9 @@ void main() { await tester.tapAnonymousSignInButton(); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); + final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s); + await tester.tapButton(expandFinder); + // Tap the options of the first group final optionsFinder = find .descendant( diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 9bc663ff12..4a117a71ff 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -617,7 +617,7 @@ extension CommonOperations on WidgetTester { ); final distanceY = getCenter(to).dy - getCenter(from).dx; await drag(from, Offset(0, distanceY)); - await pumpAndSettle(); + await pumpAndSettle(const Duration(seconds: 1)); } // tap the button with [FlowySvgData] diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index b9f59d6434..aefd5e5d36 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -115,4 +115,9 @@ class KVKeys { /// /// The value is a json string of [RecentIcons] static const String recentIcons = 'kRecentIcons'; + + /// The key for saving compact mode ids for node or databse view + /// + /// The value is a json list of id + static const String compactModeIds = 'compactModeIds'; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index df44c84286..5896c51b9b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; @@ -29,6 +27,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:go_router/go_router.dart'; @@ -59,7 +58,9 @@ class _MobileRowDetailPageState extends State { late final PageController _pageController; String get viewId => widget.databaseController.viewId; + RowCache get rowCache => widget.databaseController.rowCache; + FieldController get fieldController => widget.databaseController.fieldController; @@ -380,7 +381,9 @@ class MobileRowDetailPageContentState late final EditableCellBuilder cellBuilder; String get viewId => widget.databaseController.viewId; + RowCache get rowCache => widget.databaseController.rowCache; + FieldController get fieldController => widget.databaseController.fieldController; ValueNotifier primaryFieldId = ValueNotifier(''); @@ -542,6 +545,7 @@ class _TitleSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart index 5d0bb760fe..5317539128 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/view/view_cache.dart'; import 'package:appflowy/plugins/database/domain/database_view_service.dart'; @@ -14,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'defines.dart'; import 'row/row_cache.dart'; @@ -41,7 +40,9 @@ class GroupCallbacks { } class DatabaseLayoutSettingCallbacks { - DatabaseLayoutSettingCallbacks({required this.onLayoutSettingsChanged}); + DatabaseLayoutSettingCallbacks({ + required this.onLayoutSettingsChanged, + }); final void Function(DatabaseLayoutSettingPB) onLayoutSettingsChanged; } @@ -97,9 +98,11 @@ class DatabaseController { final List _databaseCallbacks = []; final List _groupCallbacks = []; final List _layoutCallbacks = []; + final Set> _compactModeCallbacks = {}; // Getters RowCache get rowCache => _viewCache.rowCache; + String get viewId => view.id; // Listener @@ -107,17 +110,26 @@ class DatabaseController { final DatabaseLayoutSettingListener _layoutListener; final ValueNotifier _isLoading = ValueNotifier(true); + final ValueNotifier _compactMode = ValueNotifier(true); - void setIsLoading(bool isLoading) { - _isLoading.value = isLoading; - } + void setIsLoading(bool isLoading) => _isLoading.value = isLoading; ValueNotifier get isLoading => _isLoading; + void setCompactMode(bool compactMode) { + _compactMode.value = compactMode; + for (final callback in Set.of(_compactModeCallbacks)) { + callback.call(compactMode); + } + } + + ValueNotifier get compactModeNotifier => _compactMode; + void addListener({ DatabaseCallbacks? onDatabaseChanged, DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, GroupCallbacks? onGroupChanged, + ValueChanged? onCompactModeChanged, }) { if (onLayoutSettingsChanged != null) { _layoutCallbacks.add(onLayoutSettingsChanged); @@ -130,12 +142,17 @@ class DatabaseController { if (onGroupChanged != null) { _groupCallbacks.add(onGroupChanged); } + + if (onCompactModeChanged != null) { + _compactModeCallbacks.add(onCompactModeChanged); + } } void removeListener({ DatabaseCallbacks? onDatabaseChanged, DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, GroupCallbacks? onGroupChanged, + ValueChanged? onCompactModeChanged, }) { if (onDatabaseChanged != null) { _databaseCallbacks.remove(onDatabaseChanged); @@ -148,6 +165,10 @@ class DatabaseController { if (onGroupChanged != null) { _groupCallbacks.remove(onGroupChanged); } + + if (onCompactModeChanged != null) { + _compactModeCallbacks.remove(onCompactModeChanged); + } } Future> open() async { @@ -242,6 +263,7 @@ class DatabaseController { _databaseCallbacks.clear(); _groupCallbacks.clear(); _layoutCallbacks.clear(); + _compactModeCallbacks.clear(); _isLoading.dispose(); } @@ -376,4 +398,10 @@ class DatabaseController { }, ); } + + void initCompactMode(bool enableCompactMode) { + if (_compactMode.value != enableCompactMode) { + _compactMode.value = enableCompactMode; + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart index a5dd0d9ca1..0f884a1e9a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart @@ -31,6 +31,7 @@ class DatabaseLayoutBloc @freezed class DatabaseLayoutEvent with _$DatabaseLayoutEvent { const factory DatabaseLayoutEvent.initial() = _Initial; + const factory DatabaseLayoutEvent.updateLayout(DatabaseLayoutPB layout) = _UpdateLayout; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart index bb892f4b47..e55bbb96a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; +import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; @@ -17,8 +18,17 @@ part 'tab_bar_bloc.freezed.dart'; class DatabaseTabBarBloc extends Bloc { - DatabaseTabBarBloc({required ViewPB view}) - : super(DatabaseTabBarState.initial(view)) { + DatabaseTabBarBloc({ + required ViewPB view, + required String compactModeId, + required bool enableCompactMode, + }) : super( + DatabaseTabBarState.initial( + view, + compactModeId, + enableCompactMode, + ), + ) { on( (event, emit) async { await event.when( @@ -154,10 +164,13 @@ class DatabaseTabBarBloc ) { final tabBarControllerByViewId = {...state.tabBarControllerByViewId}; for (final view in newViews) { - final controller = DatabaseTabBarController(view: view); - controller.onViewUpdated = (newView) { - add(DatabaseTabBarEvent.viewDidUpdate(newView)); - }; + final controller = DatabaseTabBarController( + view: view, + compactModeId: state.compactModeId, + enableCompactMode: state.enableCompactMode, + )..onViewUpdated = (newView) { + add(DatabaseTabBarEvent.viewDidUpdate(newView)); + }; tabBarControllerByViewId[view.id] = controller; } @@ -205,20 +218,27 @@ class DatabaseTabBarBloc @freezed class DatabaseTabBarEvent with _$DatabaseTabBarEvent { const factory DatabaseTabBarEvent.initial() = _Initial; + const factory DatabaseTabBarEvent.didLoadChildViews( List childViews, ) = _DidLoadChildViews; + const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView; + const factory DatabaseTabBarEvent.createView( DatabaseLayoutPB layout, String? name, ) = _CreateView; + const factory DatabaseTabBarEvent.renameView(String viewId, String newName) = _RenameView; + const factory DatabaseTabBarEvent.deleteView(String viewId) = _DeleteView; + const factory DatabaseTabBarEvent.didUpdateChildViews( ChildViewUpdatePB updatePB, ) = _DidUpdateChildViews; + const factory DatabaseTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; } @@ -227,19 +247,29 @@ class DatabaseTabBarState with _$DatabaseTabBarState { const factory DatabaseTabBarState({ required ViewPB parentView, required int selectedIndex, + required String compactModeId, + required bool enableCompactMode, required List tabBars, required Map tabBarControllerByViewId, }) = _DatabaseTabBarState; - factory DatabaseTabBarState.initial(ViewPB view) { + factory DatabaseTabBarState.initial( + ViewPB view, + String compactModeId, + bool enableCompactMode, + ) { final tabBar = DatabaseTabBar(view: view); return DatabaseTabBarState( parentView: view, selectedIndex: 0, + compactModeId: compactModeId, + enableCompactMode: enableCompactMode, tabBars: [tabBar], tabBarControllerByViewId: { view.id: DatabaseTabBarController( view: view, + compactModeId: compactModeId, + enableCompactMode: enableCompactMode, ), }, ); @@ -257,7 +287,9 @@ class DatabaseTabBar extends Equatable { final DatabaseTabBarItemBuilder _builder; String get viewId => view.id; + DatabaseTabBarItemBuilder get builder => _builder; + ViewLayoutPB get layout => view.layout; @override @@ -274,8 +306,18 @@ typedef OnViewChildViewChanged = void Function( ); class DatabaseTabBarController { - DatabaseTabBarController({required this.view}) - : controller = DatabaseController(view: view), + DatabaseTabBarController({ + required this.view, + required String compactModeId, + required bool enableCompactMode, + }) : controller = DatabaseController(view: view) + ..initCompactMode(enableCompactMode) + ..addListener( + onCompactModeChanged: (v) async { + compactModeEventBus + .fire(CompactModeEvent(id: compactModeId, enable: v)); + }, + ), viewListener = ViewListener(viewId: view.id) { viewListener.start( onViewChildViewsUpdated: (update) => onViewChildViewChanged?.call(update), diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 412d2bbbd8..77a26a9c58 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -285,6 +285,9 @@ class _BoardContentState extends State<_BoardContent> { @override Widget build(BuildContext context) { + final horizontalPadding = + context.read()?.horizontalPadding ?? + 0.0; return MultiBlocListener( listeners: [ BlocListener( @@ -337,68 +340,85 @@ class _BoardContentState extends State<_BoardContent> { focusScope: widget.focusScope, child: Padding( padding: const EdgeInsets.only(top: 8.0), - child: AppFlowyBoard( - boardScrollController: scrollManager, - scrollController: scrollController, - controller: context.read().boardController, - groupConstraints: const BoxConstraints.tightFor(width: 256), - config: config, - leading: HiddenGroupsColumn(margin: config.groupHeaderPadding), - trailing: context - .read() - .groupingFieldType - ?.canCreateNewGroup ?? - false - ? BoardTrailing(scrollController: scrollController) - : const HSpace(40), - headerBuilder: (_, groupData) => BlocProvider.value( - value: context.read(), - child: BoardColumnHeader( - databaseController: databaseController, - groupData: groupData, - margin: config.groupHeaderPadding, - ), - ), - footerBuilder: (_, groupData) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: context.read()), - BlocProvider.value(value: context.read()), - ], - child: BoardColumnFooter( - columnData: groupData, - boardConfig: config, - scrollManager: scrollManager, - ), - ), - cardBuilder: (context, column, columnItem) => MultiBlocProvider( - key: ValueKey("board_card_${column.id}_${columnItem.id}"), - providers: [ - BlocProvider.value( + child: ValueListenableBuilder( + valueListenable: databaseController.compactModeNotifier, + builder: (context, compactMode, _) { + return AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: scrollController, + shrinkWrap: widget.shrinkWrap, + controller: context.read().boardController, + groupConstraints: + BoxConstraints.tightFor(width: compactMode ? 196 : 256), + config: config, + leading: HiddenGroupsColumn( + shrinkWrap: widget.shrinkWrap, + margin: config.groupHeaderPadding + + EdgeInsets.only( + left: widget.shrinkWrap ? horizontalPadding : 0.0, + ), + ), + trailing: context + .read() + .groupingFieldType + ?.canCreateNewGroup ?? + false + ? BoardTrailing(scrollController: scrollController) + : const HSpace(40), + headerBuilder: (_, groupData) => BlocProvider.value( value: context.read(), + child: BoardColumnHeader( + databaseController: databaseController, + groupData: groupData, + margin: config.groupHeaderPadding, + ), ), - BlocProvider.value( - value: context.read(), - ), - BlocProvider( - create: (_) => ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return IgnorePointer( - ignoring: state.isLocked, - child: _BoardCard( - afGroupData: column, - groupItem: columnItem as GroupItem, - boardConfig: config, - notifier: widget.focusScope, - cellBuilder: cellBuilder, + footerBuilder: (_, groupData) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value( + value: context.read(), ), - ); - }, - ), - ), + ], + child: BoardColumnFooter( + columnData: groupData, + boardConfig: config, + scrollManager: scrollManager, + ), + ), + cardBuilder: (context, column, columnItem) => + MultiBlocProvider( + key: ValueKey("board_card_${column.id}_${columnItem.id}"), + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: context.read(), + ), + BlocProvider( + create: (_) => ViewLockStatusBloc(view: widget.view) + ..add(ViewLockStatusEvent.initial()), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return IgnorePointer( + ignoring: state.isLocked, + child: _BoardCard( + afGroupData: column, + groupItem: columnItem as GroupItem, + boardConfig: config, + notifier: widget.focusScope, + cellBuilder: cellBuilder, + compactMode: compactMode, + ), + ); + }, + ), + ), + ); + }, ), ), ), @@ -560,6 +580,7 @@ class _BoardCard extends StatefulWidget { required this.boardConfig, required this.cellBuilder, required this.notifier, + required this.compactMode, }); final AppFlowyGroupData afGroupData; @@ -567,6 +588,7 @@ class _BoardCard extends StatefulWidget { final AppFlowyBoardConfig boardConfig; final CardCellBuilder cellBuilder; final BoardFocusScope notifier; + final bool compactMode; @override State<_BoardCard> createState() => _BoardCardState(); @@ -653,15 +675,21 @@ class _BoardCardState extends State<_BoardCard> { return previousContainsFocus != currentContainsFocus; }, - builder: (context, focusedItems, child) => Container( - margin: widget.boardConfig.cardMargin, - decoration: _makeBoxDecoration( - context, - groupData.group.groupId, - widget.groupItem.id, - ), - child: child, - ), + builder: (context, focusedItems, child) { + final cardMargin = widget.boardConfig.cardMargin; + final margin = widget.compactMode + ? cardMargin - EdgeInsets.symmetric(horizontal: 2) + : cardMargin; + return Container( + margin: margin, + decoration: _makeBoxDecoration( + context, + groupData.group.groupId, + widget.groupItem.id, + ), + child: child, + ); + }, child: RowCard( fieldController: databaseController.fieldController, rowMeta: rowMeta, diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart index c1c127c522..1a0d1a3163 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -16,20 +13,24 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.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'; import 'package:flutter_bloc/flutter_bloc.dart'; class HiddenGroupsColumn extends StatelessWidget { const HiddenGroupsColumn({ super.key, required this.margin, + required this.shrinkWrap, }); final EdgeInsets margin; + final bool shrinkWrap; @override Widget build(BuildContext context) { @@ -85,11 +86,7 @@ class HiddenGroupsColumn extends StatelessWidget { ], ), ), - Expanded( - child: HiddenGroupList( - databaseController: databaseController, - ), - ), + _hiddenGroupList(databaseController), ], ), ), @@ -98,6 +95,14 @@ class HiddenGroupsColumn extends StatelessWidget { ); } + Widget _hiddenGroupList(DatabaseController databaseController) { + final hiddenGroupList = HiddenGroupList( + shrinkWrap: shrinkWrap, + databaseController: databaseController, + ); + return shrinkWrap ? hiddenGroupList : Expanded(child: hiddenGroupList); + } + Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) { return FlowyTooltip( message: isCollapsed @@ -125,9 +130,11 @@ class HiddenGroupList extends StatelessWidget { const HiddenGroupList({ super.key, required this.databaseController, + required this.shrinkWrap, }); final DatabaseController databaseController; + final bool shrinkWrap; @override Widget build(BuildContext context) { @@ -150,6 +157,7 @@ class HiddenGroupList extends StatelessWidget { ], ), ), + shrinkWrap: shrinkWrap, buildDefaultDragHandles: false, itemCount: state.hiddenGroups.length, itemBuilder: (_, index) => Padding( diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index 6c4b365906..dcbe626dd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -289,6 +289,7 @@ class _TitleTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 5d3ec15b46..4c9fd7bd61 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -298,6 +298,7 @@ class _GridPageContentState extends State { _GridHeader( headerScrollController: headerScrollController, editable: !context.read().state.isLocked, + shrinkWrap: widget.shrinkWrap, ), _GridRows( viewId: widget.view.id, @@ -313,10 +314,12 @@ class _GridHeader extends StatelessWidget { const _GridHeader({ required this.headerScrollController, required this.editable, + required this.shrinkWrap, }); final ScrollController headerScrollController; final bool editable; + final bool shrinkWrap; @override Widget build(BuildContext context) { @@ -324,6 +327,7 @@ class _GridHeader extends StatelessWidget { builder: (_, state) => GridHeaderSliverAdaptor( viewId: state.viewId, anchorScrollController: headerScrollController, + shrinkWrap: shrinkWrap, ), ); @@ -416,12 +420,13 @@ class _GridRowsState extends State<_GridRows> { constraints: BoxConstraints( maxWidth: GridLayout.headerWidth( context - .read() - .horizontalPadding, + .read() + .horizontalPadding * + 3, context.read().state.fields, ), ), - child: _renderList(context), + child: _shrinkWrapRenderList(context), ), ), ); @@ -452,9 +457,31 @@ class _GridRowsState extends State<_GridRows> { return Flexible(child: child); } + Widget _shrinkWrapRenderList(BuildContext context) { + final state = context.read().state; + final horizontalPadding = + context.read()?.horizontalPadding ?? + 0.0; + return ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + children: [ + widget.shrinkWrap + ? _reorderableListView(state) + : Expanded(child: _reorderableListView(state)), + if (showFloatingCalculations && !widget.shrinkWrap) ...[ + _PositionedCalculationsRow( + viewId: widget.viewId, + isAtBottom: isAtBottom, + ), + ], + ], + ); + } + Widget _renderList(BuildContext context) { final state = context.read().state; - return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart index 903ecbb864..c7402a17f9 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; + import 'sizes.dart'; class GridLayout { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart index 18883e8a0c..78a8c97dae 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart @@ -5,16 +5,26 @@ class GridSize { static double scale = 1; static double get scrollBarSize => 8 * scale; + static double get headerHeight => 36 * scale; + static double get buttonHeight => 38 * scale; + static double get footerHeight => 36 * scale; + static double get horizontalHeaderPadding => UniversalPlatform.isDesktop ? 40 * scale : 16 * scale; + static double get cellHPadding => 10 * scale; - static double get cellVPadding => 10 * scale; + + static double get cellVPadding => 8 * scale; + static double get popoverItemHeight => 26 * scale; + static double get typeOptionSeparatorHeight => 4 * scale; + static double get newPropertyButtonWidth => 140 * scale; + static double get mobileNewPropertyButtonWidth => 200 * scale; static EdgeInsets get cellContentInsets => EdgeInsets.symmetric( @@ -22,10 +32,8 @@ class GridSize { vertical: GridSize.cellVPadding, ); - static EdgeInsets get fieldContentInsets => EdgeInsets.symmetric( - horizontal: GridSize.cellHPadding, - vertical: GridSize.cellVPadding, - ); + static EdgeInsets get compactCellContentInsets => + cellContentInsets - EdgeInsets.symmetric(vertical: 2); static EdgeInsets get typeOptionContentInsets => const EdgeInsets.all(4); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index bcb9139406..e30c238f96 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -21,11 +21,13 @@ class GridHeaderSliverAdaptor extends StatefulWidget { const GridHeaderSliverAdaptor({ super.key, required this.viewId, + required this.shrinkWrap, required this.anchorScrollController, }); final String viewId; final ScrollController anchorScrollController; + final bool shrinkWrap; @override State createState() => @@ -37,6 +39,9 @@ class _GridHeaderSliverAdaptorState extends State { Widget build(BuildContext context) { final fieldController = context.read().databaseController.fieldController; + final horizontalPadding = + context.read()?.horizontalPadding ?? + 0.0; return BlocProvider( create: (context) { return GridHeaderBloc( @@ -47,9 +52,14 @@ class _GridHeaderSliverAdaptorState extends State { child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: widget.anchorScrollController, - child: _GridHeader( - viewId: widget.viewId, - fieldController: fieldController, + child: Padding( + padding: widget.shrinkWrap + ? EdgeInsets.symmetric(horizontal: horizontalPadding) + : EdgeInsets.zero, + child: _GridHeader( + viewId: widget.viewId, + fieldController: fieldController, + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index dd66f34e4d..2306767f46 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -308,14 +308,20 @@ class RowContent extends StatelessWidget { Widget _finalCellDecoration(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.basic, - child: Container( - width: GridSize.newPropertyButtonWidth, - constraints: const BoxConstraints(minHeight: 36), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: AFThemeExtension.of(context).borderColor), - ), - ), + child: ValueListenableBuilder( + valueListenable: cellBuilder.databaseController.compactModeNotifier, + builder: (context, compactMode, _) { + return Container( + width: GridSize.newPropertyButtonWidth, + constraints: BoxConstraints(minHeight: compactMode ? 32 : 36), + decoration: BoxDecoration( + border: Border( + bottom: + BorderSide(color: AFThemeExtension.of(context).borderColor), + ), + ), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart index ece2658cf2..5c33426281 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart @@ -1,14 +1,13 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../layout/sizes.dart'; import '../filter/create_filter_list.dart'; class FilterButton extends StatefulWidget { @@ -30,27 +29,25 @@ class _FilterButtonState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final textColor = state.filters.isEmpty - ? Theme.of(context).hintColor - : Theme.of(context).colorScheme.primary; - return _wrapPopover( - FlowyTextButton( - LocaleKeys.grid_settings_filter.tr(), - fontColor: textColor, - fontSize: FontSizes.s12, - fillColor: Colors.transparent, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - padding: GridSize.toolbarSettingButtonInsets, - radius: Corners.s4Border, - onPressed: () { - final bloc = context.read(); - if (bloc.state.filters.isEmpty) { - _popoverController.show(); - } else { - widget.toggleExtension.toggle(); - } - }, + MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyIconButton( + tooltipText: LocaleKeys.grid_settings_filter.tr(), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + icon: const FlowySvg(FlowySvgs.database_filter_s), + onPressed: () { + final bloc = context.read(); + if (bloc.state.filters.isEmpty) { + _popoverController.show(); + } else { + widget.toggleExtension.toggle(); + } + }, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart index 047781c6cc..c44341e214 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -3,12 +3,15 @@ import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_ import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; import 'filter_button.dart'; import 'sort_button.dart'; +import 'view_database_button.dart'; class GridSettingBar extends StatelessWidget { const GridSettingBar({ @@ -43,6 +46,8 @@ class GridSettingBar extends StatelessWidget { if (isLoading) { return const SizedBox.shrink(); } + final isReference = + Provider.of(context)?.isReference ?? false; return SizedBox( height: 20, child: Row( @@ -52,9 +57,9 @@ class GridSettingBar extends StatelessWidget { const HSpace(6), SortButton(toggleExtension: toggleExtension), const HSpace(6), - SettingButton( - databaseController: controller, - ), + SettingButton(databaseController: controller), + if (isReference) const HSpace(6), + if (isReference) ViewDatabaseButton(view: controller.view), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart index e16c851f3a..6649d53594 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart @@ -1,13 +1,12 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import '../sort/create_sort_list.dart'; @@ -27,26 +26,24 @@ class _SortButtonState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final textColor = state.sorts.isEmpty - ? Theme.of(context).hintColor - : Theme.of(context).colorScheme.primary; - return wrapPopover( - FlowyTextButton( - LocaleKeys.grid_settings_sort.tr(), - fontColor: textColor, - fontSize: FontSizes.s12, - fillColor: Colors.transparent, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - padding: GridSize.toolbarSettingButtonInsets, - radius: Corners.s4Border, - onPressed: () { - if (state.sorts.isEmpty) { - _popoverController.show(); - } else { - widget.toggleExtension.toggle(); - } - }, + MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyIconButton( + tooltipText: LocaleKeys.grid_settings_sort.tr(), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + icon: const FlowySvg(FlowySvgs.database_sort_s), + onPressed: () { + if (state.sorts.isEmpty) { + _popoverController.show(); + } else { + widget.toggleExtension.toggle(); + } + }, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart new file mode 100644 index 0000000000..93493e599f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +class ViewDatabaseButton extends StatelessWidget { + const ViewDatabaseButton({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyIconButton( + tooltipText: LocaleKeys.grid_rowPage_openAsFullPage.tr(), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + icon: const FlowySvg(FlowySvgs.database_fullscreen_s), + onPressed: () { + getIt().add( + TabsEvent.openPlugin( + plugin: view.plugin(), + view: view, + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index ea69f40540..fa5e44a5e6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -1,8 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -16,6 +16,7 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; import 'tab_bar_add_button.dart'; @@ -26,12 +27,8 @@ class TabBarHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return SizedBox( height: 35, - padding: EdgeInsets.symmetric( - horizontal: - context.read().horizontalPadding, - ), child: Stack( children: [ Positioned( @@ -332,7 +329,24 @@ class _TabBarItemButtonState extends State { enableColor: false, ); } - return Opacity(opacity: 0.6, child: icon); + final isReference = + Provider.of(context)?.isReference ?? false; + final iconWidget = Opacity(opacity: 0.6, child: icon); + return isReference + ? Stack( + children: [ + iconWidget, + const Positioned( + right: 0, + bottom: 0, + child: FlowySvg( + FlowySvgs.referenced_page_s, + blendMode: BlendMode.dstIn, + ), + ), + ], + ) + : iconWidget; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 7e009cb7f8..3554f9112e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -1,9 +1,17 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; @@ -12,7 +20,9 @@ import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; @@ -55,31 +65,84 @@ abstract class DatabaseTabBarItemBuilder { void dispose() {} } -class DatabaseTabBarView extends StatelessWidget { +class DatabaseTabBarView extends StatefulWidget { const DatabaseTabBarView({ super.key, required this.view, required this.shrinkWrap, + required this.showActions, this.initialRowId, + this.actionBuilder, + this.node, }); final ViewPB view; final bool shrinkWrap; + final BlockComponentActionBuilder? actionBuilder; + final bool showActions; + final Node? node; /// Used to open a Row on plugin load /// final String? initialRowId; + @override + State createState() => _DatabaseTabBarViewState(); +} + +class _DatabaseTabBarViewState extends State { + bool enableCompactMode = false; + bool initialed = false; + StreamSubscription? compactModeSubscription; + + String get compactModeId => widget.node?.id ?? widget.view.id; + + @override + void initState() { + super.initState(); + if (widget.node != null) { + enableCompactMode = + widget.node!.attributes[DatabaseBlockKeys.enableCompactMode] ?? false; + setState(() { + initialed = true; + }); + } else { + fetchLocalCompactMode(compactModeId).then((v) { + if (mounted) { + setState(() { + enableCompactMode = v; + initialed = true; + }); + } + }); + compactModeSubscription = + compactModeEventBus.on().listen((event) { + if (event.id != widget.view.id) return; + updateLocalCompactMode(event.enable); + }); + } + } + + @override + void dispose() { + super.dispose(); + compactModeSubscription?.cancel(); + } + @override Widget build(BuildContext context) { + if (!initialed) return Center(child: CircularProgressIndicator()); return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => DatabaseTabBarBloc(view: view) - ..add(const DatabaseTabBarEvent.initial()), + create: (_) => DatabaseTabBarBloc( + view: widget.view, + compactModeId: compactModeId, + enableCompactMode: enableCompactMode, + )..add(const DatabaseTabBarEvent.initial()), ), BlocProvider( - create: (_) => ViewBloc(view: view) + create: (_) => ViewBloc(view: widget.view) ..add( const ViewEvent.initial(), ), @@ -88,9 +151,15 @@ class DatabaseTabBarView extends StatelessWidget { child: BlocBuilder( builder: (innerContext, state) { final layout = state.tabBars[state.selectedIndex].layout; - + final isCalendar = layout == ViewLayoutPB.Calendar; + final horizontalPadding = + context.read().horizontalPadding; + final showActionWrapper = widget.showActions && + widget.actionBuilder != null && + widget.node != null; final Widget child = Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ if (UniversalPlatform.isMobile) const VSpace(12), ValueListenableBuilder( @@ -113,13 +182,39 @@ class DatabaseTabBarView extends StatelessWidget { ); } + if (showActionWrapper) { + child = BlockComponentActionWrapper( + node: widget.node!, + actionBuilder: widget.actionBuilder!, + child: Padding( + padding: EdgeInsets.only(right: horizontalPadding), + child: child, + ), + ); + } + + if (UniversalPlatform.isDesktop) { + child = Container( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + child: child, + ); + } + return child; }, ), pageSettingBarExtensionFromState(context, state), wrapContent( layout: layout, - child: pageContentFromState(context, state), + child: Padding( + padding: + (isCalendar && widget.shrinkWrap || showActionWrapper) + ? EdgeInsets.only(left: 42 - horizontalPadding) + : EdgeInsets.zero, + child: pageContentFromState(context, state), + ), ), ], ); @@ -130,8 +225,44 @@ class DatabaseTabBarView extends StatelessWidget { ); } + Future fetchLocalCompactMode(String compactModeId) async { + Set compactModeIds = {}; + try { + final localIds = await getIt().get( + KVKeys.compactModeIds, + ); + final List decodedList = jsonDecode(localIds ?? ''); + compactModeIds = Set.from(decodedList.map((item) => item as String)); + } catch (e) { + Log.warn('fetch local compact mode from id :$compactModeId failed', e); + } + return compactModeIds.contains(compactModeId); + } + + Future updateLocalCompactMode(bool enableCompactMode) async { + Set compactModeIds = {}; + try { + final localIds = await getIt().get( + KVKeys.compactModeIds, + ); + final List decodedList = jsonDecode(localIds ?? ''); + compactModeIds = Set.from(decodedList.map((item) => item as String)); + } catch (e) { + Log.warn('get compact mode ids failed', e); + } + if (enableCompactMode) { + compactModeIds.add(compactModeId); + } else { + compactModeIds.remove(compactModeId); + } + await getIt().set( + KVKeys.compactModeIds, + jsonEncode(compactModeIds.toList()), + ); + } + Widget wrapContent({required ViewLayoutPB layout, required Widget child}) { - if (shrinkWrap) { + if (widget.shrinkWrap) { if (layout.shrinkWrappable) { return child; } @@ -153,8 +284,8 @@ class DatabaseTabBarView extends StatelessWidget { context, tab.view, controller, - shrinkWrap, - initialRowId, + widget.shrinkWrap, + widget.initialRowId, ); } @@ -226,6 +357,9 @@ class DatabaseTabBarViewPlugin extends Plugin { } const kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding'; +const kDatabasePluginWidgetBuilderShowActions = 'show_actions'; +const kDatabasePluginWidgetBuilderActionBuilder = 'action_builder'; +const kDatabasePluginWidgetBuilderNode = 'node'; class DatabasePluginWidgetBuilderSize { const DatabasePluginWidgetBuilderSize({required this.horizontalPadding}); @@ -274,6 +408,11 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { final horizontalPadding = data?[kDatabasePluginWidgetBuilderHorizontalPadding] as double? ?? GridSize.horizontalHeaderPadding + 40; + final BlockComponentActionBuilder? actionBuilder = + data?[kDatabasePluginWidgetBuilderActionBuilder]; + final bool showActions = + data?[kDatabasePluginWidgetBuilderShowActions] ?? false; + final Node? node = data?[kDatabasePluginWidgetBuilderNode]; return Provider( create: (context) => DatabasePluginWidgetBuilderSize( @@ -284,6 +423,9 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { view: notifier.view, shrinkWrap: shrinkWrap, initialRowId: initialRowId, + actionBuilder: actionBuilder, + showActions: showActions, + node: node, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index e7c6150448..68c4b15d5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; @@ -16,12 +14,12 @@ import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; import '../cell/card_cell_builder.dart'; import '../cell/card_cell_skeleton/card_cell.dart'; - import 'card_bloc.dart'; import 'container/accessory.dart'; import 'container/card_container.dart'; @@ -462,7 +460,7 @@ class RowCardStyleConfiguration { const RowCardStyleConfiguration({ required this.cellStyleMap, this.showAccessory = true, - this.cardPadding = const EdgeInsets.all(8), + this.cardPadding = const EdgeInsets.all(4), this.hoverStyle, }); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart index ba71fcd30b..a91ffae42d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart @@ -40,7 +40,7 @@ class RowCardContainer extends StatelessWidget { } }, child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 42), + constraints: const BoxConstraints(minHeight: 36), child: _CardEnterRegion( shouldBuildAccessory: shouldBuildAccessory, accessories: accessories, @@ -77,8 +77,8 @@ class _CardEnterRegion extends StatelessWidget { child, if (onEnter && shouldBuildAccessory) Positioned( - top: 10.0, - right: 10.0, + top: 7.0, + right: 7.0, child: CardAccessoryContainer( accessories: accessories, onTapAccessory: onTapAccessory, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart index befe49dd6d..74abcecb3a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,22 +12,31 @@ class DesktopGridCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { - return Container( - alignment: AlignmentDirectional.centerStart, - padding: GridSize.cellContentInsets, - child: FlowyIconButton( - hoverColor: Colors.transparent, - onPressed: () => bloc.add(const CheckboxCellEvent.select()), - icon: FlowySvg( - state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: const Size.square(20), - ), - width: 20, - ), + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Container( + alignment: AlignmentDirectional.centerStart, + padding: padding, + child: FlowyIconButton( + hoverColor: Colors.transparent, + onPressed: () => bloc.add(const CheckboxCellEvent.select()), + icon: FlowySvg( + state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + size: const Size.square(20), + ), + width: 20, + ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart index f5ad4f3970..ebc4a6f976 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/checklist.dart'; @@ -15,6 +14,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, PopoverController popoverController, ) { @@ -39,15 +39,24 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { onClose: () => cellContainerNotifier.isFocus = false, child: BlocBuilder( builder: (context, state) { - return Container( - alignment: AlignmentDirectional.centerStart, - padding: GridSize.cellContentInsets, - child: state.tasks.isEmpty - ? const SizedBox.shrink() - : ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - ), + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + + return Container( + alignment: AlignmentDirectional.centerStart, + padding: padding, + child: state.tasks.isEmpty + ? const SizedBox.shrink() + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + ), + ); + }, ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart index 21bbee23ff..de7f7f5a2e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -15,6 +15,7 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, @@ -28,11 +29,11 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { child: Align( alignment: AlignmentDirectional.centerStart, child: state.fieldInfo.wrapCellContent ?? false - ? _buildCellContent(state) + ? _buildCellContent(state, compactModeNotifier) : SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: _buildCellContent(state), + child: _buildCellContent(state, compactModeNotifier), ), ), popupBuilder: (BuildContext popoverContent) { @@ -47,33 +48,44 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { ); } - Widget _buildCellContent(DateCellState state) { + Widget _buildCellContent( + DateCellState state, + ValueNotifier compactModeNotifier, + ) { final wrap = state.fieldInfo.wrapCellContent ?? false; final dateStr = getDateCellStrFromCellData( state.fieldInfo, state.cellData, ); - return Padding( - padding: GridSize.cellContentInsets, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FlowyText( - dateStr, - overflow: wrap ? null : TextOverflow.ellipsis, - maxLines: wrap ? null : 1, - ), + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Padding( + padding: padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText( + dateStr, + overflow: wrap ? null : TextOverflow.ellipsis, + maxLines: wrap ? null : 1, + ), + ), + if (state.cellData.reminderId.isNotEmpty) ...[ + const HSpace(4), + FlowyTooltip( + message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), + child: const FlowySvg(FlowySvgs.clock_alarm_s), + ), + ], + ], ), - if (state.cellData.reminderId.isNotEmpty) ...[ - const HSpace(4), - FlowyTooltip( - message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), - child: const FlowySvg(FlowySvgs.clock_alarm_s), - ), - ], - ], - ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart index f66d106ed0..b070af7cc7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart @@ -72,7 +72,7 @@ class GridMediaCellSkin extends IEditableMediaCellSkin { if (!isMobile && wrapContent) { return Padding( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.symmetric(horizontal: 4), child: SizedBox( width: double.infinity, child: Wrap( @@ -233,7 +233,7 @@ class _FilePreviewRender extends StatelessWidget { height: 28, width: 28, clipBehavior: Clip.antiAlias, - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: AFThemeExtension.of(context).greyHover, borderRadius: BorderRadius.circular(4), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart index 04368bc725..7a6f3e63bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart @@ -1,6 +1,6 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,27 +11,37 @@ class DesktopGridNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: context.watch().state.wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + + return TextField( + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: context.watch().state.wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: padding, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart index 88d5fdf9d3..dda3183b59 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart @@ -17,6 +17,7 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, @@ -39,9 +40,14 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { }, child: Align( alignment: AlignmentDirectional.centerStart, - child: state.wrap - ? _buildWrapRows(context, state.rows) - : _buildNoWrapRows(context, state.rows), + child: ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + return state.wrap + ? _buildWrapRows(context, state.rows, compactMode) + : _buildNoWrapRows(context, state.rows, compactMode); + }, + ), ), ); } @@ -49,9 +55,12 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget _buildWrapRows( BuildContext context, List rows, + bool compactMode, ) { return Padding( - padding: GridSize.cellContentInsets, + padding: compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets, child: Wrap( runSpacing: 4, spacing: 4.0, @@ -73,6 +82,7 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget _buildNoWrapRows( BuildContext context, List rows, + bool compactMode, ) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart index 8cebb2d77a..b599acc4f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart @@ -15,6 +15,7 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { @@ -35,63 +36,92 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { return Align( alignment: AlignmentDirectional.centerStart, child: state.wrap - ? _buildWrapOptions(context, state.selectedOptions) - : _buildNoWrapOptions(context, state.selectedOptions), + ? _buildWrapOptions( + context, + state.selectedOptions, + compactModeNotifier, + ) + : _buildNoWrapOptions( + context, + state.selectedOptions, + compactModeNotifier, + ), ); }, ), ); } - Widget _buildWrapOptions(BuildContext context, List options) { - return Padding( - padding: GridSize.cellContentInsets, - child: Wrap( - runSpacing: 4, - children: options.map( - (option) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - ), - ); - }, - ).toList(), - ), + Widget _buildWrapOptions( + BuildContext context, + List options, + ValueNotifier compactModeNotifier, + ) { + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Padding( + padding: padding, + child: Wrap( + runSpacing: 4, + children: options.map( + (option) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectOptionTag( + option: option, + padding: EdgeInsets.symmetric( + vertical: compactMode ? 2 : 4, + horizontal: 8, + ), + ), + ); + }, + ).toList(), + ), + ); + }, ); } Widget _buildNoWrapOptions( BuildContext context, List options, + ValueNotifier compactModeNotifier, ) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: Padding( - padding: GridSize.cellContentInsets, - child: Row( - mainAxisSize: MainAxisSize.min, - children: options.map( - (option) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 1, - horizontal: 8, - ), - ), - ); - }, - ).toList(), - ), + child: ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Padding( + padding: padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: options.map( + (option) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric( + vertical: 1, + horizontal: 8, + ), + ), + ); + }, + ).toList(), + ), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart index b5d915b022..1f3ded0109 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart @@ -11,6 +11,7 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -27,58 +28,69 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { onExit: (p) => Provider.of(context, listen: false) .onEnter = false, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: GridSize.headerHeight, - ), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: TextField( - controller: textEditingController, - enabled: false, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), + child: ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: compactMode + ? GridSize.headerHeight - 4 + : GridSize.headerHeight, ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.cellVPadding, - ), - child: Consumer( - builder: ( - BuildContext context, - SummaryMouseNotifier notifier, - Widget? child, - ) { - if (notifier.onEnter) { - return SummaryCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ).positioned(right: 0, bottom: 8), - ], - ), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: TextField( + controller: textEditingController, + enabled: false, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: null, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: padding, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.cellVPadding, + ), + child: Consumer( + builder: ( + BuildContext context, + SummaryMouseNotifier notifier, + Widget? child, + ) { + if (notifier.onEnter) { + return SummaryCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ).positioned(right: 0, bottom: compactMode ? 4 : 8), + ], + ), + ); + }, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart index f28cb756c8..75c973d886 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart @@ -13,43 +13,52 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return Padding( - padding: GridSize.cellContentInsets, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _IconOrEmoji(), - Expanded( - child: TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: context.watch().state.wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: context - .read() - .cellController - .fieldInfo - .isPrimary - ? FontWeight.w500 - : null, + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, data) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _IconOrEmoji(), + Expanded( + child: TextField( + controller: textEditingController, + focusNode: focusNode, + maxLines: context.watch().state.wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: context + .read() + .cellController + .fieldInfo + .isPrimary + ? FontWeight.w500 + : null, + ), + decoration: const InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + isCollapsed: true, ), - decoration: const InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - isCollapsed: true, + ), ), - ), + ], ), - ], - ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart index a1690310d4..8a1fd92499 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart @@ -1,6 +1,6 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -11,29 +11,41 @@ class DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { return Container( alignment: AlignmentDirectional.centerStart, child: state.wrap - ? _buildCellContent(state) + ? _buildCellContent(state, compactModeNotifier) : SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: _buildCellContent(state), + child: _buildCellContent(state, compactModeNotifier), ), ); } - Widget _buildCellContent(TimestampCellState state) { - return Padding( - padding: GridSize.cellContentInsets, - child: FlowyText( - state.dateStr, - overflow: state.wrap ? null : TextOverflow.ellipsis, - maxLines: state.wrap ? null : 1, - ), + Widget _buildCellContent( + TimestampCellState state, + ValueNotifier compactModeNotifier, + ) { + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return Padding( + padding: padding, + child: FlowyText( + state.dateStr, + overflow: state.wrap ? null : TextOverflow.ellipsis, + maxLines: state.wrap ? null : 1, + ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart index aece28373c..102b491f52 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart @@ -11,6 +11,7 @@ class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -18,68 +19,79 @@ class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin { return ChangeNotifierProvider( create: (_) => TranslateMouseNotifier(), builder: (context, child) { - return MouseRegion( - cursor: SystemMouseCursors.click, - opaque: false, - onEnter: (p) => - Provider.of(context, listen: false) - .onEnter = true, - onExit: (p) => - Provider.of(context, listen: false) - .onEnter = false, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: GridSize.headerHeight, - ), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), + return ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => + Provider.of(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(context, listen: false) + .onEnter = false, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: compactMode + ? GridSize.headerHeight - 4 + : GridSize.headerHeight, ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.cellVPadding, - ), - child: Consumer( - builder: ( - BuildContext context, - TranslateMouseNotifier notifier, - Widget? child, - ) { - if (notifier.onEnter) { - return TranslateCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ).positioned(right: 0, bottom: 8), - ], - ), - ), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: TextField( + controller: textEditingController, + readOnly: true, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: null, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: padding, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.cellVPadding, + ), + child: Consumer( + builder: ( + BuildContext context, + TranslateMouseNotifier notifier, + Widget? child, + ) { + if (notifier.onEnter) { + return TranslateCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ).positioned(right: 0, bottom: compactMode ? 4 : 8), + ], + ), + ), + ); + }, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart index 17a3519d3d..935716e686 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart @@ -21,6 +21,7 @@ class DesktopGridURLSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -28,28 +29,36 @@ class DesktopGridURLSkin extends IEditableURLCellSkin { ) { return BlocSelector( selector: (state) => state.wrap, - builder: (context, wrap) => TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, + builder: (context, wrap) => ValueListenableBuilder( + valueListenable: compactModeNotifier, + builder: (context, compactMode, _) { + final padding = compactMode + ? GridSize.compactCellContentInsets + : GridSize.cellContentInsets; + return TextField( + controller: textEditingController, + focusNode: focusNode, + maxLines: wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + decoration: InputDecoration( + contentPadding: padding, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).hintColor), + isDense: true, ), - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), - isDense: true, - ), - onTapOutside: (_) => focusNode.unfocus(), + onTapOutside: (_) => focusNode.unfocus(), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart index bb15cd5d9f..1e56c5160e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class DesktopRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index d7e1d64fc3..b10d63d2d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -24,6 +24,7 @@ class DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart index f10f9a9279..f1b5f14975 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -13,6 +13,7 @@ class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart index 97f8f80569..e90fc85549 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class DesktopRowDetailNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart index 89db702d22..d760d3ac29 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart @@ -16,6 +16,7 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart index 3d41a824ce..ff84744c27 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; @@ -8,6 +6,7 @@ import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart' import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/select_option.dart'; @@ -18,6 +17,7 @@ class DesktopRowDetailSelectOptionCellSkin Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart index d8a8902a8c..30cd54832d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart @@ -10,6 +10,7 @@ class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart index b1e10e4da3..9511c2f871 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class DesktopRowDetailTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart index af212c6dfd..6fc534f313 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -10,6 +10,7 @@ class DesktopRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart index 00d7372027..ee9d7e7300 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart @@ -1,8 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,6 +14,7 @@ class DesktopRowDetailURLSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart index 1c7bab9f92..a374417b3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart @@ -10,6 +10,7 @@ class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart index 4b7bd2c442..ab421b8925 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,6 +27,7 @@ abstract class IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ); @@ -71,6 +72,7 @@ class _CheckboxCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, state, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart index 4cdebee36d..fbed429642 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,6 +28,7 @@ abstract class IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, PopoverController popoverController, ); @@ -72,6 +73,7 @@ class GridChecklistCellState extends GridCellState { child: widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, _popover, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart index 877b4c6dfb..e61c759f48 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart @@ -1,12 +1,12 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; @@ -33,6 +33,7 @@ abstract class IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, @@ -79,6 +80,7 @@ class _DateCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, state, _popover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart index b218c78195..4d2bfdf627 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,6 +29,7 @@ abstract class IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -89,6 +90,7 @@ class _NumberCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart index 4e39900abf..67ca6275a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,6 +28,7 @@ abstract class IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, @@ -74,6 +75,7 @@ class _RelationCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, state, _popover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart index b45018f4f5..f7e8b6f435 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; @@ -31,6 +31,7 @@ abstract class IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ); @@ -79,6 +80,7 @@ class _SelectOptionCellState extends GridCellState { child: widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, _popover, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart index d3b43b0d17..7a086b2a35 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; @@ -22,6 +19,8 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; abstract class IEditableSummaryCellSkin { @@ -39,6 +38,7 @@ abstract class IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -98,6 +98,7 @@ class _SummaryCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart index 7666919c49..3ea622374e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,6 +29,7 @@ abstract class IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -92,6 +93,7 @@ class _TextCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart index 6c00e8b4b4..2fc9d049cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,6 +28,7 @@ abstract class IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ); @@ -74,6 +75,7 @@ class _TimestampCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, state, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart index b4ff26d946..b273419aed 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; @@ -22,6 +19,8 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; abstract class IEditableTranslateCellSkin { @@ -39,6 +38,7 @@ abstract class IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -98,6 +98,7 @@ class _TranslateCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart index 0b4afc086e..0dc7779e55 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -40,6 +40,7 @@ abstract class IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -121,6 +122,7 @@ class _GridURLCellState extends GridEditableTextCell { child: widget.skin.build( context, widget.cellContainerNotifier, + widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart index 8859372c2a..e9ac19c874 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/checkbox.dart'; @@ -10,6 +10,7 @@ class MobileGridCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart index ff9f83319f..c56d28e1a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart @@ -1,9 +1,9 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,6 +15,7 @@ class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart index 43b6b7f347..5686e09295 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart @@ -1,18 +1,18 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class MobileGridDateCellSkin extends IEditableDateCellSkin { @override Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart index c02cf6aa8d..310c0b5692 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/number.dart'; @@ -9,6 +9,7 @@ class MobileGridNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart index 0951e2fb0d..69e9b20104 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class MobileGridRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart index 61f67fec4f..010974e49a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart @@ -1,8 +1,8 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -16,6 +16,7 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart index 0da8d6bc64..e48c56d74d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart @@ -12,6 +12,7 @@ class MobileGridSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart index 40e8c35319..43a4fe49d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart @@ -11,6 +11,7 @@ class MobileGridTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart index d9e020eece..68209e7e05 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -10,6 +10,7 @@ class MobileGridTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart index 3a7b44cbc5..4288136734 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart @@ -12,6 +12,7 @@ class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index cddb821943..0dbe5474c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -13,6 +13,7 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart index 279e790913..ade82e8c5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; import '../editable_cell_skeleton/checkbox.dart'; @@ -12,6 +11,7 @@ class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart index daf085e2ce..75eee9a560 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart @@ -17,6 +17,7 @@ class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart index 8671bacd8f..0256ee25cf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart @@ -2,9 +2,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -14,6 +14,7 @@ class MobileRowDetailDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart index 6e32fbbbdc..430044fb5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class MobileRowDetailNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart index 61a39a867a..c3e8b82867 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart @@ -1,7 +1,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -10,6 +10,7 @@ class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart index 9dafb6afe0..7d4eb71f9d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -19,6 +19,7 @@ class MobileRowDetailSelectOptionCellSkin Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart index 1e709bdeb9..9974220b96 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart @@ -9,6 +9,7 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart index 1cdde84c27..fc8f816103 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ class MobileRowDetailTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart index 7ddda492c9..f3f800e994 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,6 +12,7 @@ class MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart index a1e4b4bf29..c2d84b3d2e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart @@ -9,6 +9,7 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index f87b225492..9bb91255aa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -4,9 +4,9 @@ import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.da import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flowy_infra/theme_extension.dart'; import '../editable_cell_skeleton/url.dart'; @@ -15,6 +15,7 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart index c979ee0829..a218e1ed68 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart @@ -3,17 +3,25 @@ import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class DatabaseViewWidget extends StatefulWidget { const DatabaseViewWidget({ super.key, required this.view, this.shrinkWrap = true, + required this.showActions, + required this.node, + this.actionBuilder, }); final ViewPB view; final bool shrinkWrap; + final BlockComponentActionBuilder? actionBuilder; + final bool showActions; + final Node node; @override State createState() => _DatabaseViewWidgetState(); @@ -50,14 +58,26 @@ class _DatabaseViewWidgetState extends State { @override Widget build(BuildContext context) { + double? horizontalPadding = 0.0; + final databasePluginWidgetBuilderSize = + Provider.of(context); + if (view.layout == ViewLayoutPB.Grid || view.layout == ViewLayoutPB.Board) { + horizontalPadding = 40.0; + } + if (databasePluginWidgetBuilderSize != null) { + horizontalPadding = databasePluginWidgetBuilderSize.horizontalPadding; + } + return ValueListenableBuilder( valueListenable: _layoutTypeChangeNotifier, builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget( shrinkWrap: widget.shrinkWrap, context: PluginContext(), data: { - kDatabasePluginWidgetBuilderHorizontalPadding: - view.layout == ViewLayoutPB.Grid ? 40.0 : 0.0, + kDatabasePluginWidgetBuilderHorizontalPadding: horizontalPadding, + kDatabasePluginWidgetBuilderActionBuilder: widget.actionBuilder, + kDatabasePluginWidgetBuilderShowActions: widget.showActions, + kDatabasePluginWidgetBuilderNode: widget.node, }, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index 7a888b96ed..f1486094bf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart @@ -57,25 +57,28 @@ class DatabaseGroupList extends StatelessWidget { final children = [ if (showHideUngroupedToggle) ...[ - SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: Row( - children: [ - Expanded( - child: FlowyText( - LocaleKeys.board_showUngrouped.tr(), - ), - ), - Toggle( - value: !state.layoutSettings.hideUngroupedColumn, - onChanged: (value) => - _updateLayoutSettings(state.layoutSettings, !value), - padding: EdgeInsets.zero, - ), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + resetHoverOnRebuild: false, + text: FlowyText( + LocaleKeys.board_showUngrouped.tr(), + lineHeight: 1.0, + ), + onTap: () { + _updateLayoutSettings( + state.layoutSettings, + !state.layoutSettings.hideUngroupedColumn, + ); + }, + rightIcon: Toggle( + value: !state.layoutSettings.hideUngroupedColumn, + onChanged: (value) => + _updateLayoutSettings(state.layoutSettings, !value), + padding: EdgeInsets.zero, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart index 2e1260fe3d..333ff0fe96 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart @@ -1,6 +1,5 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; - import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -58,7 +57,7 @@ class CellContainer extends StatelessWidget { } }, child: Container( - constraints: BoxConstraints(maxWidth: width, minHeight: 36), + constraints: BoxConstraints(maxWidth: width, minHeight: 32), decoration: _makeBoxDecoration(context, isFocus), child: container, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 5b77e93f8e..8d64c537c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -624,6 +624,7 @@ class _TitleSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart index f6c1a87782..8bd181b427 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart @@ -85,39 +85,51 @@ class _RowDetailPageState extends State { ], child: BlocBuilder( builder: (context, state) => Stack( + fit: StackFit.expand, children: [ - ListView( - controller: scrollController, - physics: const ClampingScrollPhysics(), - children: [ - RowBanner( - databaseController: widget.databaseController, - rowController: widget.rowController, - cellBuilder: cellBuilder, - allowOpenAsFullPage: widget.allowOpenAsFullPage, - userProfile: widget.userProfile, - ), - const VSpace(16), - Padding( - padding: const EdgeInsets.only(left: 40, right: 60), - child: RowPropertyList( - cellBuilder: cellBuilder, - viewId: widget.databaseController.viewId, - fieldController: - widget.databaseController.fieldController, - ), - ), - const VSpace(20), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 60), - child: Divider(height: 1.0), - ), - const VSpace(20), - RowDocument( + Positioned.fill( + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverToBoxAdapter( + child: Column( + children: [ + RowBanner( + databaseController: widget.databaseController, + rowController: widget.rowController, + cellBuilder: cellBuilder, + allowOpenAsFullPage: widget.allowOpenAsFullPage, + userProfile: widget.userProfile, + ), + const VSpace(16), + Padding( + padding: + const EdgeInsets.only(left: 40, right: 60), + child: RowPropertyList( + cellBuilder: cellBuilder, + viewId: widget.databaseController.viewId, + fieldController: + widget.databaseController.fieldController, + ), + ), + const VSpace(20), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 60), + child: Divider(height: 1.0), + ), + const VSpace(20), + ], + ), + ), + ]; + }, + body: RowDocument( viewId: widget.rowController.viewId, rowId: widget.rowController.rowId, ), - ], + ), ), Positioned( top: calculateActionsOffset( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart index e3d30249ac..0489db8907 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; @@ -102,24 +103,26 @@ class _RowEditor extends StatelessWidget { return BlocProvider( create: (context) => ViewInfoBloc(view: view), - child: IntrinsicHeight( - child: Container( - constraints: const BoxConstraints(minHeight: 300), - child: Provider( - create: (_) { - final context = SharedEditorContext(); - context.isInDatabaseRowPage = true; - return context; - }, - dispose: (_, editorContext) => editorContext.dispose(), - child: EditorDropHandler( + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: Provider( + create: (_) { + final context = SharedEditorContext(); + context.isInDatabaseRowPage = true; + return context; + }, + dispose: (_, editorContext) => editorContext.dispose(), + child: EditorDropHandler( + viewId: view.id, + editorState: editorState, + isLocalMode: context.read().isLocalMode, + dropManagerState: context.read(), + child: EditorTransactionService( viewId: view.id, editorState: editorState, - isLocalMode: context.read().isLocalMode, - dropManagerState: context.read(), - child: EditorTransactionService( - viewId: view.id, - editorState: editorState, + child: Provider( + create: (context) => + DatabasePluginWidgetBuilderSize(horizontalPadding: 0), child: AppFlowyEditorPage( shrinkWrap: true, autoFocus: false, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart index c8c23365de..dd600241db 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart @@ -1,35 +1,33 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/layout/layout_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../grid/presentation/layout/sizes.dart'; - -class DatabaseLayoutSelector extends StatefulWidget { +class DatabaseLayoutSelector extends StatelessWidget { const DatabaseLayoutSelector({ super.key, required this.viewId, - required this.currentLayout, + required this.databaseController, }); final String viewId; - final DatabaseLayoutPB currentLayout; + final DatabaseController databaseController; - @override - State createState() => _DatabaseLayoutSelectorState(); -} - -class _DatabaseLayoutSelectorState extends State { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => DatabaseLayoutBloc( - viewId: widget.viewId, - databaseLayout: widget.currentLayout, + viewId: viewId, + databaseLayout: databaseController.databaseLayout, )..add(const DatabaseLayoutEvent.initial()), child: BlocBuilder( builder: (context, state) { @@ -44,14 +42,56 @@ class _DatabaseLayoutSelectorState extends State { ), ) .toList(); - - return ListView.separated( - shrinkWrap: true, - itemCount: cells.length, + return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), - itemBuilder: (_, int index) => cells[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ListView.separated( + shrinkWrap: true, + itemCount: cells.length, + padding: EdgeInsets.zero, + itemBuilder: (_, int index) => cells[index], + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + ), + Container( + height: 1, + margin: EdgeInsets.fromLTRB(8, 4, 8, 0), + color: AFThemeExtension.of(context).borderColor, + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 2), + child: SizedBox( + height: 30, + child: FlowyButton( + resetHoverOnRebuild: false, + text: FlowyText( + LocaleKeys.grid_settings_compactMode.tr(), + lineHeight: 1.0, + ), + onTap: () { + databaseController.setCompactMode( + !databaseController.compactModeNotifier.value, + ); + }, + rightIcon: ValueListenableBuilder( + valueListenable: databaseController.compactModeNotifier, + builder: (context, compactMode, child) { + return Toggle( + value: compactMode, + onChanged: (value) => + databaseController.setCompactMode(value), + padding: EdgeInsets.zero, + ); + }, + ), + ), + ), + ), + ], + ), ); }, ), @@ -76,7 +116,7 @@ class DatabaseViewLayoutCell extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: SizedBox( - height: GridSize.popoverItemHeight, + height: 30, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart index 409454848a..5f40959c02 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart @@ -3,8 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart'; import 'package:appflowy/plugins/database/widgets/group/database_group.dart'; +import 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -53,7 +53,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { final popover = switch (this) { DatabaseSettingAction.showLayout => DatabaseLayoutSelector( viewId: databaseController.viewId, - currentLayout: databaseController.databaseLayout, + databaseController: databaseController, ), DatabaseSettingAction.showGroup => DatabaseGroupList( viewId: databaseController.viewId, @@ -88,6 +88,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { iconData(), color: Theme.of(context).iconTheme.color, ), + rightIcon: FlowySvg(FlowySvgs.database_settings_arrow_right_s), ), ), popupBuilder: (context) => popover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart index 4d31e5a79d..36a6436b2a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart @@ -1,13 +1,11 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class SettingButton extends StatefulWidget { const SettingButton({super.key, required this.databaseController}); @@ -29,15 +27,17 @@ class _SettingButtonState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 8), triggerActions: PopoverTriggerFlags.none, - child: FlowyTextButton( - LocaleKeys.settings_title.tr(), - fontColor: Theme.of(context).hintColor, - fontSize: FontSizes.s12, - fillColor: Colors.transparent, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - padding: GridSize.toolbarSettingButtonInsets, - radius: Corners.s4Border, - onPressed: _popoverController.show, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyIconButton( + tooltipText: LocaleKeys.settings_title.tr(), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + icon: const FlowySvg(FlowySvgs.settings_s), + onPressed: _popoverController.show, + ), ), popupBuilder: (_) => DatabaseSettingsList(databaseController: widget.databaseController), diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 788834d074..7f4493a999 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -141,6 +141,7 @@ class _TitleSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, + ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart new file mode 100644 index 0000000000..eaee989bbc --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart @@ -0,0 +1,13 @@ +import 'package:event_bus/event_bus.dart'; + +EventBus compactModeEventBus = EventBus(); + +class CompactModeEvent { + CompactModeEvent({ + required this.id, + required this.enable, + }); + + final String id; + final bool enable; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 20049236ef..7e3b5347da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -15,6 +15,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/page_block/custom_page_block_component.dart'; + /// A global configuration for the editor. class EditorGlobalConfiguration { /// Whether to enable the drag menu in the editor. @@ -215,6 +217,9 @@ void _customBlockOptionActions( } else { top += 2.0; } + if (overflowTypes.contains(type)) { + top = top / 2; + } return ValueListenableBuilder( valueListenable: EditorGlobalConfiguration.enableDragMenu, builder: (_, enableDragMenu, child) { @@ -268,7 +273,7 @@ Map _buildBlockComponentBuilderMap( bool alwaysDistributeSimpleTableColumnWidths = false, }) { final customBlockComponentBuilderMap = { - PageBlockKeys.type: PageBlockComponentBuilder(), + PageBlockKeys.type: CustomPageBlockComponentBuilder(), ParagraphBlockKeys.type: _buildParagraphBlockComponentBuilder( context, configuration, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart index fcbd7ea6ca..6323f675cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart @@ -42,7 +42,7 @@ class BlockActionList extends StatelessWidget { editorState: editorState, blockComponentBuilder: blockComponentBuilder, ), - const HSpace(8.0), + const HSpace(5.0), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index 9f25e4745d..f96a06b21d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -1,16 +1,9 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -79,14 +72,7 @@ class _BuiltInPageWidgetState extends State { return MouseRegion( onEnter: (_) => widget.editorState.service.scrollService?.disable(), onExit: (_) => widget.editorState.service.scrollService?.enable(), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildMenu(context, viewPB), - Flexible(child: _buildPage(context, viewPB)), - ], - ), + child: _buildPage(context, viewPB), ); } @@ -98,64 +84,10 @@ class _BuiltInPageWidgetState extends State { widget.editorState.service.selectionService.clearSelection(); } }, - child: widget.builder(view), - ); - } - - Widget _buildMenu(BuildContext context, ViewPB view) { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // information - FlowyIconButton( - tooltipText: LocaleKeys.tooltip_referencePage.tr( - namedArgs: {'name': view.layout.name}, - ), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - icon: const FlowySvg( - FlowySvgs.information_s, - ), - ), - // setting - const Space(7, 0), - PopoverActionList<_ActionWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, - actions: _ActionType.values - .map((action) => _ActionWrapper(action)) - .toList(), - buildChild: (controller) => FlowyIconButton( - tooltipText: LocaleKeys.tooltip_openMenu.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - icon: const FlowySvg( - FlowySvgs.settings_s, - ), - onPressed: () => controller.show(), - ), - onSelected: (action, controller) async { - switch (action.inner) { - case _ActionType.viewDatabase: - getIt().add( - TabsEvent.openPlugin( - plugin: view.plugin(), - view: view, - ), - ); - break; - case _ActionType.delete: - final transaction = widget.editorState.transaction; - transaction.deleteNode(widget.node); - await widget.editorState.apply(transaction); - break; - } - controller.close(); - }, - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: widget.builder(view), + ), ); } @@ -165,23 +97,3 @@ class _BuiltInPageWidgetState extends State { await widget.editorState.apply(transaction); } } - -enum _ActionType { viewDatabase, delete } - -class _ActionWrapper extends ActionCell { - _ActionWrapper(this.inner); - - final _ActionType inner; - - Widget? icon(Color iconColor) => null; - - @override - String get name { - switch (inner) { - case _ActionType.viewDatabase: - return LocaleKeys.tooltip_viewDataBase.tr(); - case _ActionType.delete: - return LocaleKeys.disclosureAction_delete.tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart index bc0dcc17e3..86df2ae172 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart @@ -1,5 +1,9 @@ +import 'dart:async'; + import 'package:appflowy/plugins/database/widgets/database_view_widget.dart'; +import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -13,8 +17,14 @@ class DatabaseBlockKeys { static const String parentID = 'parent_id'; static const String viewID = 'view_id'; + static const String enableCompactMode = 'enable_compact_mode'; } +const overflowTypes = { + DatabaseBlockKeys.gridType, + DatabaseBlockKeys.boardType, +}; + class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { DatabaseViewBlockComponentBuilder({ super.configuration, @@ -65,35 +75,62 @@ class _DatabaseBlockComponentWidgetState @override BlockComponentConfiguration get configuration => widget.configuration; + late StreamSubscription compactModeSubscription; + EditorState? editorState; + + @override + void initState() { + super.initState(); + compactModeSubscription = + compactModeEventBus.on().listen((event) { + if (event.id != node.id) return; + final newAttributes = { + ...node.attributes, + DatabaseBlockKeys.enableCompactMode: event.enable, + }; + final theEditorState = editorState; + if (theEditorState == null) return; + final transaction = theEditorState.transaction; + transaction.updateNode(node, newAttributes); + theEditorState.apply(transaction); + }); + } + + @override + void dispose() { + super.dispose(); + compactModeSubscription.cancel(); + editorState = null; + } + @override Widget build(BuildContext context) { final editorState = Provider.of(context, listen: false); + this.editorState = editorState; Widget child = BuiltInPageWidget( node: widget.node, editorState: editorState, - builder: (view) => DatabaseViewWidget(key: ValueKey(view.id), view: view), - ); - - child = Padding( - padding: padding, - child: FocusScope( - skipTraversal: true, - onFocusChange: (value) { - if (value && keepEditorFocusNotifier.value == 0) { - context.read().selection = null; - } - }, - child: child, + builder: (view) => Provider.value( + value: ReferenceState(true), + child: DatabaseViewWidget( + key: ValueKey(view.id), + view: view, + actionBuilder: widget.actionBuilder, + showActions: widget.showActions, + node: widget.node, + ), ), ); - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: widget.node, - actionBuilder: widget.actionBuilder!, - child: child, - ); - } + child = FocusScope( + skipTraversal: true, + onFocusChange: (value) { + if (value && keepEditorFocusNotifier.value == 0) { + context.read().selection = null; + } + }, + child: child, + ); if (!editorState.editable) { child = IgnorePointer( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 12fe821459..c18e01b3dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -160,6 +160,7 @@ class _DocumentCoverWidgetState extends State { final offset = _calculateIconLeft(context, constraints); return Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index 90a4c73013..398d158e9a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -62,6 +62,12 @@ Node pageMentionNode(String viewId) { ); } +class ReferenceState { + ReferenceState(this.isReference); + + final bool isReference; +} + class MentionPageBlock extends StatefulWidget { const MentionPageBlock({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart new file mode 100644 index 0000000000..5d7028f6e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart @@ -0,0 +1,116 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/block_component/base_component/widget/ignore_parent_gesture.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CustomPageBlockComponentBuilder extends BlockComponentBuilder { + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + return CustomPageBlockComponent( + key: blockComponentContext.node.key, + node: blockComponentContext.node, + header: blockComponentContext.header, + footer: blockComponentContext.footer, + ); + } +} + +class CustomPageBlockComponent extends BlockComponentStatelessWidget { + const CustomPageBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + this.header, + this.footer, + }); + + final Widget? header; + final Widget? footer; + + @override + Widget build(BuildContext context) { + final editorState = context.read(); + final scrollController = context.read(); + final items = node.children; + + if (scrollController == null || scrollController.shrinkWrap) { + return SingleChildScrollView( + child: Builder( + builder: (context) { + final scroller = Scrollable.maybeOf(context); + if (scroller != null) { + editorState.updateAutoScroller(scroller); + } + return Column( + children: [ + if (header != null) header!, + ...items.map( + (e) => Container( + constraints: BoxConstraints( + maxWidth: + editorState.editorStyle.maxWidth ?? double.infinity, + ), + padding: editorState.editorStyle.padding, + child: editorState.renderer.build(context, e), + ), + ), + if (footer != null) footer!, + ], + ); + }, + ), + ); + } else { + int extentCount = 0; + if (header != null) extentCount++; + if (footer != null) extentCount++; + + return ScrollablePositionedList.builder( + shrinkWrap: scrollController.shrinkWrap, + itemCount: items.length + extentCount, + itemBuilder: (context, index) { + editorState.updateAutoScroller(Scrollable.of(context)); + if (header != null && index == 0) { + return IgnoreEditorSelectionGesture( + child: header!, + ); + } + + if (footer != null && index == (items.length - 1) + extentCount) { + return IgnoreEditorSelectionGesture( + child: footer!, + ); + } + + final childNode = items[index - (header != null ? 1 : 0)]; + final isOverflowType = overflowTypes.contains(childNode.type); + + final item = Container( + constraints: BoxConstraints( + maxWidth: editorState.editorStyle.maxWidth ?? double.infinity, + ), + padding: isOverflowType + ? EdgeInsets.zero + : editorState.editorStyle.padding, + child: editorState.renderer.build( + context, + childNode, + ), + ); + + return isOverflowType ? item : Center(child: item); + }, + itemScrollController: scrollController.itemScrollController, + scrollOffsetController: scrollController.scrollOffsetController, + itemPositionsListener: scrollController.itemPositionsListener, + scrollOffsetListener: scrollController.scrollOffsetListener, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 99275b9a8e..9fc0a02cec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -1,8 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; class ToggleListBlockKeys { @@ -172,6 +174,7 @@ class _ToggleListBlockComponentWidgetState ); bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false; + int? get level => node.attributes[ToggleListBlockKeys.level] as int?; @override @@ -194,12 +197,16 @@ class _ToggleListBlockComponentWidgetState color: backgroundColor, ), ), - NestedListWidget( - indentPadding: indentPadding, - child: buildComponent(context), - children: editorState.renderer.buildList( - context, - widget.node.children, + Provider( + create: (context) => + DatabasePluginWidgetBuilderSize(horizontalPadding: 0.0), + child: NestedListWidget( + indentPadding: indentPadding, + child: buildComponent(context), + children: editorState.renderer.buildList( + context, + widget.node.children, + ), ), ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart index 7f3eff9d34..59ee55b980 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -30,8 +30,11 @@ class ShareButton extends StatelessWidget { ), if (view.layout.isDatabaseView) BlocProvider( - create: (context) => DatabaseTabBarBloc(view: view) - ..add(const DatabaseTabBarEvent.initial()), + create: (context) => DatabaseTabBarBloc( + view: view, + compactModeId: view.id, + enableCompactMode: false, + )..add(const DatabaseTabBarEvent.initial()), ), ], child: BlocListener( diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 7f19e976d4..fcd991fcf9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -334,6 +334,7 @@ extension ViewLayoutExtension on ViewLayoutPB { bool get shrinkWrappable => switch (this) { ViewLayoutPB.Grid => true, + ViewLayoutPB.Board => true, _ => false, }; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index fad0defff3..043b11cde5 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -81,8 +81,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5517c8704c0dbeaeda5601e9baadb4cc2b29990d" - resolved-ref: "5517c8704c0dbeaeda5601e9baadb4cc2b29990d" + ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 + resolved-ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git version: "0.1.2" @@ -618,6 +618,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + event_bus: + dependency: "direct main" + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" expandable: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index a93b261419..6dc31cf237 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: appflowy_board: git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: 5517c8704c0dbeaeda5601e9baadb4cc2b29990d + ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 appflowy_editor: appflowy_editor_plugins: appflowy_popover: @@ -135,6 +135,7 @@ dependencies: synchronized: ^3.1.0+1 table_calendar: ^3.0.9 time: ^2.1.3 + event_bus: ^2.0.1 toastification: ^2.0.0 universal_platform: ^1.1.0 diff --git a/frontend/resources/flowy_icons/16x/database_filter.svg b/frontend/resources/flowy_icons/16x/database_filter.svg new file mode 100644 index 0000000000..8ca4fa3e51 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/database_filter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/database_fullscreen.svg b/frontend/resources/flowy_icons/16x/database_fullscreen.svg new file mode 100644 index 0000000000..7e8794cbc5 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/database_fullscreen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg b/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg new file mode 100644 index 0000000000..00fe13b7fc --- /dev/null +++ b/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/database_sort.svg b/frontend/resources/flowy_icons/16x/database_sort.svg new file mode 100644 index 0000000000..3df5682f3a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/database_sort.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index d2c9eaaf23..3a78ef813f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1392,6 +1392,7 @@ "filterBy": "Filter by", "typeAValue": "Type a value...", "layout": "Layout", + "compactMode": "Compact mode", "databaseLayout": "Layout", "viewList": { "zero": "0 views", diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index 8535f46024..d40ab58d72 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -94,7 +94,10 @@ pub struct BoardLayoutSetting { impl BoardLayoutSetting { pub fn new() -> Self { - Self::default() + Self { + hide_ungrouped_column: false, + collapse_hidden_groups: true, + } } } From 637c043f5b58ad4288581407ec6042e52c7cfd37 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:34:07 +0800 Subject: [PATCH 064/384] fix: ai writer launch review 0.8.5 (#7445) * fix: improve writing not working * fix: show insert below and discard buttons even in overflow * fix: incorrect predefined format initialization * fix: generate image * chore: multi-line related questions * fix: add to undo history * fix: disable keyboard service when using ai writer * fix: disable drag nodes * fix: strikethrough text after accepting * fix: undo --- .../lib/ai/service/ai_prompt_input_bloc.dart | 9 ++ .../presentation/chat_related_question.dart | 2 +- .../ai/ai_writer_block_component.dart | 82 +++++++++++-------- .../ai/ai_writer_toolbar_item.dart | 1 + .../ai/operations/ai_writer_cubit.dart | 14 ++-- .../ai/operations/ai_writer_entities.dart | 9 -- .../base/markdown_text_robot.dart | 33 ++++---- frontend/rust-lib/flowy-ai/src/completion.rs | 2 +- 8 files changed, 87 insertions(+), 65 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index e80b196db6..23d8879275 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -44,6 +44,7 @@ class AIPromptInputBloc extends Bloc { final aiType = chatState.pluginState.state == RunningStatePB.Running ? AIType.localAI : AIType.appflowyAI; + emit( state.copyWith( aiType: aiType, @@ -69,9 +70,17 @@ class AIPromptInputBloc extends Bloc { ); }, toggleShowPredefinedFormat: () { + final predefinedFormat = + !state.showPredefinedFormats && state.predefinedFormat == null + ? PredefinedFormat( + imageFormat: ImageFormat.text, + textFormat: TextFormat.paragraph, + ) + : null; emit( state.copyWith( showPredefinedFormats: !state.showPredefinedFormats, + predefinedFormat: predefinedFormat, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index deba1cb96d..9e4cc603b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -70,7 +70,7 @@ class RelatedQuestionItem extends StatelessWidget { child: FlowyText( question, lineHeight: 1.4, - overflow: TextOverflow.ellipsis, + maxLines: null, ), ), expandText: false, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index fd1d0c4c82..a6a4c80b5d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -169,6 +169,7 @@ class _AIWriterBlockComponentState extends State { return GestureDetector( behavior: hitTestBehavior, onTap: () => onTapOutside(), + onTapDown: (_) => onTapOutside(), ); }, ), @@ -300,46 +301,59 @@ class OverlayContent extends StatelessWidget { child: Container( constraints: BoxConstraints(maxHeight: 140), width: double.infinity, - child: SingleChildScrollView( - padding: EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 24.0, - padding: EdgeInsets.symmetric(horizontal: 6.0), - alignment: AlignmentDirectional.centerStart, - child: FlowyText( - state.command.i18n, - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF666D76), + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + physics: ClampingScrollPhysics(), + padding: EdgeInsets.only(top: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 24.0, + padding: + EdgeInsets.symmetric(horizontal: 6.0), + alignment: + AlignmentDirectional.centerStart, + child: FlowyText( + state.command.i18n, + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF666D76), + ), + ), + const VSpace(4.0), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 6.0), + child: AIMarkdownText( + markdown: markdownText, + ), + ), + ], ), ), + ), + if (showSuggestionPopup) ...[ const VSpace(4.0), - Padding( - padding: EdgeInsets.symmetric(horizontal: 6.0), - child: AIMarkdownText( - markdown: markdownText, + SuggestionActionBar( + actions: _getSuggestedActions( + currentCommand: state.command, + hasSelection: hasSelection, ), + onTap: (action) { + context + .read() + .runResponseAction(action); + }, ), - if (showSuggestionPopup) ...[ - const VSpace(4.0), - SuggestionActionBar( - actions: _getSuggestedActions( - currentCommand: state.command, - hasSelection: hasSelection, - ), - onTap: (action) { - context - .read() - .runResponseAction(action); - }, - ), - ], ], - ), + const VSpace(8.0), + ], ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 96fb8289d3..714bd5f4b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -187,6 +187,7 @@ class ImproveWritingButton extends StatelessWidget { onPressed: () { if (_isAIEnabled(editorState)) { keepEditorFocusNotifier.increase(); + _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { showToastNotification( context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index e1c6efb337..bf46dd0e9a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -29,7 +29,9 @@ class AiWriterCubit extends Cubit { initialCommand, isFirstRun: true, ), - ); + ) { + editorState.service.keyboardService?.disable(); + } final String documentId; final EditorState editorState; @@ -40,10 +42,12 @@ class AiWriterCubit extends Cubit { final ValueNotifier> selectedSourcesNotifier; (String, PredefinedFormat?)? _previousPrompt; + bool acceptReplacesOriginal = false; @override Future close() async { selectedSourcesNotifier.dispose(); + editorState.service.keyboardService?.enable(); await super.close(); } @@ -212,7 +216,7 @@ class AiWriterCubit extends Cubit { if (action case SuggestionAction.accept || SuggestionAction.keep) { await _textRobot.persist(); - if (state.command.acceptWillReplace) { + if (acceptReplacesOriginal) { final nodes = editorState.getNodesInSelection(selection); final transaction = editorState.transaction..deleteNodes(nodes); await editorState.apply( @@ -339,7 +343,6 @@ class AiWriterCubit extends Cubit { ); }, onEnd: () async { - editorState.service.keyboardService?.enable(); if (state case GeneratingAiWriterState _) { await _textRobot.stop( attributes: ApplySuggestionFormatType.replace.attributes, @@ -348,7 +351,6 @@ class AiWriterCubit extends Cubit { } }, onError: (error) async { - editorState.service.keyboardService?.enable(); emit(ErrorAiWriterState(command, error: error)); }, ); @@ -369,6 +371,8 @@ class AiWriterCubit extends Cubit { return; } + acceptReplacesOriginal = true; + final stream = await _aiService.streamCompletion( objectId: documentId, text: await editorState.getMarkdownInSelection(selection), @@ -413,7 +417,6 @@ class AiWriterCubit extends Cubit { } }, onError: (error) async { - editorState.service.keyboardService?.enable(); emit(ErrorAiWriterState(command, error: error)); }, ); @@ -451,7 +454,6 @@ class AiWriterCubit extends Cubit { } }, onEnd: () async { - editorState.service.keyboardService?.enable(); if (state case final GeneratingAiWriterState generatingState) { emit( ReadyAiWriterState( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart index 349d7cb419..e0cc944b3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -108,15 +108,6 @@ enum AiWriterCommand { makeShorter => CompletionTypePB.MakeShorter, makeLonger => CompletionTypePB.MakeLonger, }; - - bool get acceptWillReplace => switch (this) { - AiWriterCommand.fixSpellingAndGrammar || - AiWriterCommand.improveWriting || - AiWriterCommand.makeLonger || - AiWriterCommand.makeShorter => - true, - _ => false, - }; } enum ApplySuggestionFormatType { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index ac170fef00..36936f36a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -162,12 +162,6 @@ class MarkdownTextRobot { return; } - final node = editorState.getNodeAtPath(position.path); - if (node == null) { - Log.error("Cannot find node at position: ${position.path}"); - return; - } - // Convert markdown and deep copy the nodes, prevent ing the linked // entities from being changed final documentNodes = customMarkdownToDocument( @@ -184,29 +178,40 @@ class MarkdownTextRobot { if (newNodes.isEmpty) { return; } - final transaction = editorState.transaction - ..insertNodes(position.path, newNodes) + + final deleteTransaction = editorState.transaction ..deleteNodes(getInsertedNodes()); + await editorState.apply( + deleteTransaction, + options: ApplyOptions( + inMemoryUpdate: inMemoryUpdate, + recordUndo: false, + ), + ); + + final insertTransaction = editorState.transaction + ..insertNodes(position.path, newNodes); + final lastDelta = newNodes.lastOrNull?.delta; if (lastDelta != null) { - transaction.afterSelection = Selection.collapsed( + insertTransaction.afterSelection = Selection.collapsed( Position( path: position.path.nextNPath(newNodes.length - 1), offset: lastDelta.length, ), ); - } - if (!updateSelection) { - transaction.afterSelection = null; + if (!updateSelection) { + insertTransaction.afterSelection = null; + } } await editorState.apply( - transaction, + insertTransaction, options: ApplyOptions( inMemoryUpdate: inMemoryUpdate, - recordUndo: false, + recordUndo: !inMemoryUpdate, ), ); diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 28c64d8a8f..38f4f565e6 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -104,7 +104,7 @@ impl CompletionTask { custom_prompt: None, metadata: Some(CompletionMetadata { object_id: self.context.object_id, - workspace_id: None, + workspace_id: Some(self.workspace_id.clone()), rag_ids: Some(self.context.rag_ids), }), format: self.context.format.map(Into::into).unwrap_or_default(), From 3bf4f080c52693ed1a4b90c47df38d08ca4350a9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Mar 2025 16:34:26 +0800 Subject: [PATCH 065/384] feat: auto calculate the column width when resizing (#7448) * feat: calculate the column width auto * fix: ai writer table issue --- .../lib/plugins/document/document_page.dart | 7 +-- .../document/presentation/editor_page.dart | 3 +- .../actions/drag_to_reorder/util.dart | 46 ++++++++++++++++--- .../base/markdown_text_robot.dart | 2 +- .../simple_columns_block_component.dart | 14 +++--- .../simple_columns_block_constant.dart | 2 +- 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 83fc24c204..5c695fa508 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -190,9 +190,10 @@ class _DocumentPageState extends State ), header: buildCoverAndIcon(context, state), initialSelection: initialSelection, - placeholderText: (node) => node.type == ParagraphBlockKeys.type - ? LocaleKeys.editor_slashPlaceHolder.tr() - : '', + placeholderText: (node) => + node.type == ParagraphBlockKeys.type && !node.isInTable + ? LocaleKeys.editor_slashPlaceHolder.tr() + : '', ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 20bba4f083..d2716fd716 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -162,13 +162,12 @@ class _AppFlowyEditorPageState extends State editorLaunchUrl = (url) { if (url != null) { - afLaunchUrlString(url); + afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); } return Future.value(true); }; - effectiveScrollController = widget.scrollController ?? ScrollController(); // disable the color parse in the HTML decoder. DocumentHTMLDecoder.enableColorParse = false; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index bf9b1bdc7a..f38d1d7257 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -51,17 +52,36 @@ Future dragToMoveNode( final transaction = editorState.transaction; final targetNodeParent = targetNode.parentColumnsBlock; if (targetNodeParent != null) { + final length = targetNodeParent.children.length; final columnNode = simpleColumnNode( children: [node.deepCopy()], + width: (node.rect.width * 1 / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), ); + for (final column in targetNodeParent.children) { + final width = + column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; + transaction.updateNode(column, { + ...column.attributes, + SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), + }); + } + transaction.insertNode(targetNode.path.next, columnNode); transaction.deleteNode(node); } else { + final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [targetNode.deepCopy()]), - simpleColumnNode(children: [node.deepCopy()]), + simpleColumnNode(children: [targetNode.deepCopy()], width: width), + simpleColumnNode(children: [node.deepCopy()], width: width), ], ); @@ -82,14 +102,28 @@ Future dragToMoveNode( final targetNodeParent = targetNode.parentColumnsBlock; if (targetNodeParent != null) { // find the previous sibling node of the target node - final width = (node.rect.width / - (targetNode.parentColumnsBlock?.children.length ?? 2)) - - 16; + final length = targetNodeParent.children.length; final columnNode = simpleColumnNode( children: [node.deepCopy()], - width: width, + width: (node.rect.width * 1 / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), ); + for (final column in targetNodeParent.children) { + final width = + column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; + transaction.updateNode(column, { + ...column.attributes, + SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), + }); + } + transaction.insertNode(targetNode.path.previous, columnNode); transaction.deleteNode(node); } else { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 36936f36a9..e818e9437b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -235,7 +235,7 @@ class MarkdownTextRobot { List? children; if (node.children.isNotEmpty) { children = node.children - .map((child) => _styleDelta(node: node, attributes: attributes)) + .map((child) => _styleDelta(node: child, attributes: attributes)) .toList(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index 849d5afd4c..43fd779d33 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -88,13 +88,12 @@ class ColumnsBlockComponentState extends State late final EditorState editorState = context.read(); - @override - void initState() { - super.initState(); - } + final ScrollController scrollController = ScrollController(); @override void dispose() { + scrollController.dispose(); + super.dispose(); } @@ -102,6 +101,7 @@ class ColumnsBlockComponentState extends State Widget build(BuildContext context) { Widget child = SingleChildScrollView( scrollDirection: Axis.horizontal, + controller: scrollController, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: _buildChildren(), @@ -111,6 +111,7 @@ class ColumnsBlockComponentState extends State if (UniversalPlatform.isDesktop) { // only show the scrollbar on desktop child = Scrollbar( + controller: scrollController, child: child, ); } @@ -149,8 +150,9 @@ class ColumnsBlockComponentState extends State final children = []; for (var i = 0; i < node.children.length; i++) { final childNode = node.children[i]; - final width = childNode.attributes[SimpleColumnBlockKeys.width] ?? - SimpleColumnsBlockConstants.minimumColumnWidth; + final width = + childNode.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; Widget child = editorState.renderer.build(context, childNode); child = SizedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart index 977974a32e..d8820f8613 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart @@ -1,6 +1,6 @@ class SimpleColumnsBlockConstants { const SimpleColumnsBlockConstants._(); - static const double minimumColumnWidth = 128; + static const double minimumColumnWidth = 128.0; static const bool enableDebugBorder = false; } From bbec60ff02192ec321f384a000d2aba9e7fcb56e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:17:42 +0800 Subject: [PATCH 066/384] fix(flutter_desktop): page name overflow in search (#7450) --- .../command_palette/widgets/recent_view_tile.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart index 000de7c9db..645b9696c8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -27,9 +27,11 @@ class RecentViewTile extends StatelessWidget { children: [ icon, const HSpace(6), - FlowyText( - view.nameOrDefault, - overflow: TextOverflow.ellipsis, + Expanded( + child: FlowyText( + view.nameOrDefault, + overflow: TextOverflow.ellipsis, + ), ), ], ), From e9371029f3d8e99bb3582a173a30457182adefe4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Mar 2025 17:17:57 +0800 Subject: [PATCH 067/384] chore: update changelog (#7451) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e8559b73..3c47e17aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ # Release Notes +## Version 0.8.5 - 04/03/2025 +### New Features +- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu +- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more +- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen +### Bug Fixes +- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document +- Fixed a bug preventing the relation field in databases from opening +- Fixed an issue where links in documents were unclickable on Linux + ## Version 0.8.4 - 18/02/2025 ### New Features - Switch AI mode on mobile From 2dd7e5937f330e75c8ac220ba0e322e6d01ede96 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:20:26 +0800 Subject: [PATCH 068/384] fix: incorrect popover position (#7452) * fix: incorrect popover position * fix: tests --------- Co-authored-by: Lucas.Xu --- .../actions/block_action_button.dart | 32 +++++++------------ .../actions/block_action_option_button.dart | 1 - 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart index 3561bd33a0..9e0b241ab3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart @@ -1,8 +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:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class BlockActionButton extends StatelessWidget { @@ -23,15 +22,17 @@ class BlockActionButton extends StatelessWidget { @override Widget build(BuildContext context) { - Widget child = MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: IgnoreParentGestureWidget( - onPress: onPointerDown, - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.deferToChild, + return FlowyTooltip( + richMessage: showTooltip ? richMessage : null, + child: FlowyIconButton( + width: 18.0, + hoverColor: Colors.transparent, + iconColorOnHover: Theme.of(context).iconTheme.color, + onPressed: onTap, + icon: MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, child: FlowySvg( svg, size: const Size.square(18.0), @@ -40,14 +41,5 @@ class BlockActionButton extends StatelessWidget { ), ), ); - - if (showTooltip) { - child = FlowyTooltip( - richMessage: richMessage, - child: child, - ); - } - - return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index 29dabd0da1..4efb1a55b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -50,7 +50,6 @@ class _BlockOptionButtonState extends State { child: BlocBuilder( builder: (context, _) => PopoverActionList( actions: _buildPopoverActions(context), - popoverMutex: PopoverMutex(), animationDuration: Durations.short3, slideDistance: 5, beginScaleFactor: 1.0, From 4b2389dafdead6fcaebf6770e66b94407fbc2087 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:23:28 +0800 Subject: [PATCH 069/384] chore: bump client api (#7455) --- .../helpers/handle_open_workspace_error.dart | 2 +- frontend/rust-lib/Cargo.lock | 38 ++++++------------- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 2 +- frontend/rust-lib/flowy-error/src/code.rs | 7 +++- frontend/rust-lib/flowy-error/src/errors.rs | 2 +- .../flowy-error/src/impl_from/cloud.rs | 3 +- 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index 0aeb92fc18..2e8e4feeae 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -15,7 +15,7 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { getIt().pushWorkspaceErrorScreen(context, userFolder, error); break; case ErrorCode.InvalidEncryptSecret: - case ErrorCode.HttpError: + case ErrorCode.NetworkError: showToastNotification( context, message: error.msg, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 80833d70d0..8f6726606e 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "bytes", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "again", "anyhow", @@ -794,7 +794,6 @@ dependencies = [ "arc-swap", "async-trait", "base64 0.22.1", - "bincode", "brotli", "bytes", "chrono", @@ -823,7 +822,6 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_repr", "serde_urlencoded", "shared-entity", "thiserror 1.0.64", @@ -843,7 +841,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +854,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "futures-channel", "futures-util", @@ -1128,13 +1126,12 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "bincode", "bytes", "chrono", - "client-websocket", "collab", "collab-entity", "collab-rt-protocol", @@ -1143,9 +1140,7 @@ dependencies = [ "prost-build", "protoc-bin-vendored", "serde", - "serde_json", "serde_repr", - "thiserror 1.0.64", "tokio-tungstenite", "yrs", ] @@ -1153,7 +1148,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "async-trait", @@ -1548,11 +1543,8 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ - "anyhow", - "app-error", - "appflowy-ai-client", "bincode", "bytes", "chrono", @@ -2970,28 +2962,24 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", - "futures-util", "getrandom 0.2.10", "gotrue-entity", "infra", "reqwest 0.12.9", "serde", "serde_json", - "tokio", "tracing", ] [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ - "anyhow", "app-error", - "chrono", "jsonwebtoken", "lazy_static", "serde", @@ -3598,7 +3586,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "bytes", @@ -6140,7 +6128,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" dependencies = [ "anyhow", "app-error", @@ -6152,14 +6140,12 @@ dependencies = [ "futures", "gotrue-entity", "infra", - "log", "pin-project", "reqwest 0.12.9", "serde", "serde_json", "serde_repr", "thiserror 1.0.64", - "tracing", "uuid", "validator 0.19.0", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d1806fbcb9..fd51a4a59c 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80eea34e73cfffe71e484cbaddb2d547076eaf09" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80eea34e73cfffe71e484cbaddb2d547076eaf09" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 12b230580a..17ffb29a0d 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -221,7 +221,7 @@ impl Chat { answer_stream_buffer.lock().await.push_str(&value); // trace!("[Chat] stream answer: {}", value); if let Err(err) = answer_sink.send(format!("data:{}", value)).await { - error!("Failed to stream answer: {}", err); + error!("Failed to stream answer via IsolateSink: {}", err); } }, QuestionStreamValue::Metadata { value } => { diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index acc145767e..e97393d094 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -164,8 +164,8 @@ pub enum ErrorCode { #[error("Sql error")] SqlError = 58, - #[error("Http error")] - HttpError = 59, + #[error("Network error")] + NetworkError = 59, #[error("The content should not be empty")] UnexpectedEmpty = 60, @@ -368,6 +368,9 @@ pub enum ErrorCode { #[error("View is locked")] ViewIsLocked = 126, + + #[error("Request timeout")] + RequestTimeout = 127, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 507905dbad..e7b6fdd439 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -131,7 +131,7 @@ impl FlowyError { static_flowy_error!(serde, ErrorCode::Serde); static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound); static_flowy_error!(payload_none, ErrorCode::UnexpectedEmpty); - static_flowy_error!(http, ErrorCode::HttpError); + static_flowy_error!(http, ErrorCode::NetworkError); static_flowy_error!( unexpect_calendar_field_type, ErrorCode::UnexpectedCalendarFieldType diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index 4c4380338b..53617c8c36 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -18,7 +18,8 @@ impl From for FlowyError { AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, - AppErrorCode::NetworkError => ErrorCode::HttpError, + AppErrorCode::NetworkError => ErrorCode::NetworkError, + AppErrorCode::RequestTimeout => ErrorCode::RequestTimeout, AppErrorCode::PayloadTooLarge => ErrorCode::PayloadTooLarge, AppErrorCode::UserUnAuthorized => ErrorCode::UserUnauthorized, AppErrorCode::WorkspaceLimitExceeded => ErrorCode::WorkspaceLimitExceeded, From 796fda159ef032e09355e68d1e9c47c2c429f656 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:59:53 +0800 Subject: [PATCH 070/384] chore: bump client api (#7463) --- frontend/rust-lib/Cargo.lock | 24 ++++++++++---------- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai/src/completion.rs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 8f6726606e..50cfd1cc7e 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bytes", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "again", "anyhow", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "collab-entity", "collab-rt-entity", @@ -854,7 +854,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "futures-channel", "futures-util", @@ -1126,7 +1126,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bincode", @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "async-trait", @@ -1543,7 +1543,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "bincode", "bytes", @@ -2962,7 +2962,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2977,7 +2977,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "app-error", "jsonwebtoken", @@ -3586,7 +3586,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bytes", @@ -6128,7 +6128,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e1d5b0edeeaa0894f7d060e9b121a3d0673090e#3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fd51a4a59c..27c81a9f96 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e1d5b0edeeaa0894f7d060e9b121a3d0673090e" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 38f4f565e6..0dd145d853 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -94,7 +94,7 @@ impl CompletionTask { CompletionTypePB::MakeLonger => CompletionType::MakeLonger, CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, CompletionTypePB::ExplainSelected => CompletionType::Explain, - _ => CompletionType::ContinueWriting, + CompletionTypePB::UserQuestion => CompletionType::UserQuestion, }; let _ = sink.send("start:".to_string()).await; From 8d8fc91391801933a3cdd5cd4807d4446dd93710 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 6 Mar 2025 12:33:12 +0800 Subject: [PATCH 071/384] fix: title position working incorrectly with document width setting (#7465) --- .../presentation/editor_plugins/header/cover_title.dart | 2 -- .../editor_plugins/header/document_cover_widget.dart | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart index 097276a394..2c5062d408 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; @@ -93,7 +92,6 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> { builder: (context, state) { final appearance = context.read().state; return Container( - padding: EditorStyleCustomizer.documentPaddingWithOptionMenu, constraints: BoxConstraints(maxWidth: width), child: Theme( data: Theme.of(context).copyWith( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index c18e01b3dd..c4f0491596 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -196,7 +196,7 @@ class _DocumentCoverWidgetState extends State { ], ), Padding( - padding: const EdgeInsets.only(bottom: 12.0), + padding: EdgeInsets.fromLTRB(offset, 0, offset, 12), child: MouseRegion( onEnter: (event) => isCoverTitleHovered.value = true, onExit: (event) => isCoverTitleHovered.value = false, From 8046177d843cb79764053521126f034544280a80 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 6 Mar 2025 12:33:53 +0800 Subject: [PATCH 072/384] fix: simple columns issues (#7466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "feat: use flutter_distrubutor to build linux and macos packages (#7392)" This reverts commit 6dc45c983071a1e6678e6aeda22a6323f73c5bda. * fix: linux link issue * fix: outline doesn't work well in columns * fix: cannot drag a block under a table that’s in the second column --- .github/workflows/release.yml | 230 +++++++------- .../appflowy_flutter/distribute_options.yaml | 31 +- .../lib/core/helpers/url_launcher.dart | 6 + .../draggable_option_button.dart | 13 + .../actions/drag_to_reorder/util.dart | 10 + .../outline/outline_block_component.dart | 11 +- .../appflowy_flutter/linux/CMakeLists.txt | 2 +- .../linux/packaging/appimage/make_config.yaml | 22 -- .../linux/packaging/deb/make_config.yaml | 3 +- .../linux/packaging/rpm/make_config.yaml | 3 +- .../macos/packaging/assets/background.jpg | Bin 20532 -> 0 bytes .../macos/packaging/dmg/make_config.json | 26 -- .../macos/packaging/dmg/make_config.yaml | 17 - frontend/scripts/docker-buildfiles/Dockerfile | 6 +- .../flatpack-buildfiles/dbus-interface.xml | 6 +- .../io.appflowy.AppFlowy.desktop | 4 +- .../io.appflowy.AppFlowy.metainfo.xml | 34 +- .../io.appflowy.AppFlowy.service | 4 +- .../scripts/flatpack-buildfiles/launcher.sh | 4 +- .../flutter_release_build/build_linux.sh | 243 -------------- .../flutter_release_build/build_macos.sh | 297 ------------------ .../appimage/AppImageBuilder.yml | 4 +- .../linux_distribution/deb/AppFlowy.desktop | 2 +- .../linux_distribution/deb/DEBIAN/postinst | 4 +- .../linux_distribution/deb/DEBIAN/postrm | 4 +- .../linux_distribution/deb/build_deb.sh | 6 +- .../linux_distribution/flatpak/README.md | 2 +- .../io.appflowy.AppFlowy.metainfo.xml | 34 +- .../packaging/io.appflowy.AppFlowy.service | 2 +- .../linux_distribution/packaging/launcher.sh | 6 +- frontend/scripts/linux_installer/postinst | 4 +- 31 files changed, 207 insertions(+), 833 deletions(-) delete mode 100644 frontend/appflowy_flutter/linux/packaging/appimage/make_config.yaml delete mode 100644 frontend/appflowy_flutter/macos/packaging/assets/background.jpg delete mode 100644 frontend/appflowy_flutter/macos/packaging/dmg/make_config.json delete mode 100644 frontend/appflowy_flutter/macos/packaging/dmg/make_config.yaml delete mode 100755 frontend/scripts/flutter_release_build/build_linux.sh delete mode 100755 frontend/scripts/flutter_release_build/build_macos.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 438169f72d..918a2018f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -128,9 +128,9 @@ jobs: runs-on: ${{ matrix.job.os }} needs: create-release env: - MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }} + MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip - MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.dmg + MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64 strategy: fail-fast: false matrix: @@ -158,22 +158,46 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - brew install p7zip cargo install --force --locked cargo-make cargo install --force --locked duckscript_cli - - name: Import codesign certificate - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} - p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }} - - name: Build AppFlowy working-directory: frontend run: | flutter config --enable-macos-desktop - flutter pub global activate flutter_distributor - sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch x86_64 --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} + dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }} + + - name: Codesign AppFlowy + run: | + echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12 + security create-keychain -p action build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p action build.keychain + security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain + /usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v + + - name: Create macOS dmg + run: | + brew install create-dmg + create-dmg \ + --volname ${{ env.MACOS_DMG_NAME }} \ + --hide-extension "AppFlowy.app" \ + --background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \ + --window-size 600 450 \ + --icon-size 94 \ + --icon "AppFlowy.app" 141 249 \ + --app-drop-link 458 249 \ + "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \ + "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" + + - name: Notarize AppFlowy + run: | + xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait + + - name: Archive Asset + working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} + run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app - name: Upload Asset uses: actions/upload-release-asset@v1 @@ -191,82 +215,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }} - asset_name: ${{ env.MACOS_DMG_NAME }} - asset_content_type: application/octet-stream - - build-for-macOS-arm64: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - needs: create-release - env: - MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }} - MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-arm64.zip - MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-arm64.dmg - strategy: - fail-fast: false - matrix: - job: - - { - targets: "aarch64-apple-darwin", - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - targets: ${{ matrix.job.targets }} - components: rustfmt - - - name: Install prerequisites - working-directory: frontend - run: | - brew install p7zip - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli - - - name: Import codesign certificate - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} - p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }} - - - name: Build AppFlowy - working-directory: frontend - run: | - flutter config --enable-macos-desktop - dart pub global activate flutter_distributor - sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch arm64 --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} - - - name: Upload Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_AARCH64_ZIP_NAME }} - asset_name: ${{ env.MACOS_AARCH64_ZIP_NAME }} - asset_content_type: application/octet-stream - - - name: Upload DMG Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }} - asset_name: ${{ env.MACOS_DMG_NAME }} + asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg + asset_name: ${{ env.MACOS_DMG_NAME }}.dmg asset_content_type: application/octet-stream build-for-macOS-universal: @@ -274,9 +224,9 @@ jobs: runs-on: ${{ matrix.job.os }} needs: create-release env: - MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }} + MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip - MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.dmg + MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal strategy: fail-fast: false matrix: @@ -306,22 +256,46 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - brew install p7zip cargo install --force --locked cargo-make cargo install --force --locked duckscript_cli - - name: Import codesign certificate - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} - p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }} - - name: Build AppFlowy working-directory: frontend run: | flutter config --enable-macos-desktop - dart pub global activate flutter_distributor - sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch universal --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} + sh scripts/flutter_release_build/build_universal_package_for_macos.sh ${{ github.ref_name }} + + - name: Codesign AppFlowy + run: | + echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12 + security create-keychain -p action build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p action build.keychain + security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain + /usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v + + - name: Create macOS dmg + run: | + brew install create-dmg + create-dmg \ + --volname ${{ env.MACOS_DMG_NAME }} \ + --hide-extension "AppFlowy.app" \ + --background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \ + --window-size 600 450 \ + --icon-size 94 \ + --icon "AppFlowy.app" 141 249 \ + --app-drop-link 458 249 \ + "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \ + "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" + + - name: Notarize AppFlowy + run: | + xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait + + - name: Archive Asset + working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} + run: zip --symlinks -qr ${{ env.MACOS_AARCH64_ZIP_NAME }} AppFlowy.app - name: Upload Asset uses: actions/upload-release-asset@v1 @@ -339,8 +313,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }} - asset_name: ${{ env.MACOS_DMG_NAME }} + asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg + asset_name: ${{ env.MACOS_DMG_NAME }}.dmg asset_content_type: application/octet-stream build-for-linux: @@ -348,12 +322,14 @@ jobs: runs-on: ${{ matrix.job.os }} needs: create-release env: - LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }} + LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release + LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm + LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm + LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage - LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.zip - LINUX_PACKAGE_TAR_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz + LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz strategy: fail-fast: false @@ -362,7 +338,7 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-22.04, + os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-x86_64, } @@ -388,24 +364,14 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - # Install dependencies sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo apt-get update - sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev - sudo apt install rpm patchelf locate - sudo add-apt-repository universe - sudo apt install libfuse2 - - # Install appimagetool - wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x appimagetool - mv appimagetool /usr/local/bin/ - - # Install cargo-make and duckscript + sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev + sudo apt-get install keybinder-3.0 + sudo apt-get install -y alien libnotify-dev source $HOME/.cargo/env cargo install --force --locked cargo-make cargo install --force --locked duckscript_cli - rustup target add ${{ matrix.job.target }} - name: Install gcc-aarch64-linux-gnu @@ -418,8 +384,30 @@ jobs: working-directory: frontend run: | flutter config --enable-linux-desktop - dart pub global activate flutter_distributor - ./scripts/flutter_release_build/build_linux.sh --build_type all --build_arch x86_64 --version ${{ github.ref_name }} + dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }} + + - name: Archive Asset + working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} + run: tar -czf ${{ env.LINUX_ZIP_NAME }} * + + - name: Build Linux package (.deb) + working-directory: frontend + run: | + sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }} + + - name: Build Linux package (.rpm) + working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} + run: | + sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }} + cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }} + + - name: Build Linux package (.AppImage) + working-directory: frontend + continue-on-error: true + run: | + sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }} + cd .. + cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} - name: Upload Asset id: upload-release-asset @@ -428,11 +416,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_ZIP_NAME }} + asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }} asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }} asset_content_type: application/octet-stream - - name: Upload DEB package + - name: Upload Debian package id: upload-release-asset-install-package-deb uses: actions/upload-release-asset@v1 env: diff --git a/frontend/appflowy_flutter/distribute_options.yaml b/frontend/appflowy_flutter/distribute_options.yaml index 9ed8a31822..60f603a938 100644 --- a/frontend/appflowy_flutter/distribute_options.yaml +++ b/frontend/appflowy_flutter/distribute_options.yaml @@ -1,35 +1,12 @@ -output: build/ +output: dist/ releases: - - name: prod + - name: dev jobs: - - name: release-prod-linux-deb + - name: release-dev-linux-deb package: platform: linux target: deb - - name: release-prod-linux-rpm + - name: release-dev-linux-rpm package: platform: linux target: rpm - - name: release-prod-linux-appimage - package: - platform: linux - target: appimage - - name: release-prod-linux-zip - package: - platform: linux - target: zip - - # Because the flutter_distribute plugin does not support the deep code-signing for macos, we need to manually sign the app. - # So we don't use the flutter_distribute plugin to distribute the macos app. - # - name: release-prod-macos-dmg - # package: - # platform: macos - # target: dmg - # - name: release-prod-macos-pkg - # package: - # platform: macos - # target: pkg - - name: release-prod-macos-zip - package: - platform: macos - target: zip diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index 2b0bc7b345..a27ab07e9d 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:open_filex/open_filex.dart'; import 'package:string_validator/string_validator.dart'; +import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher.dart' as launcher; typedef OnFailureCallback = void Function(Uri uri); @@ -38,6 +39,11 @@ Future afLaunchUri( ); } + // on Linux, add http scheme to the url if it is not present + if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) { + uri = Uri.parse('https://$url'); + } + // try to launch the uri directly bool result; try { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart index 044f0ca595..f09781591f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart @@ -108,6 +108,12 @@ class _DraggableOptionButtonState extends State { } } + // return simple table block if the target node is in a simple table block + final parentSimpleTableNode = targetNode.parentTableNode; + if (parentSimpleTableNode != null) { + return parentSimpleTableNode; + } + return targetNode; }, builder: (context, data) { @@ -160,9 +166,16 @@ class _DraggableOptionButtonState extends State { } } + // return simple table block if the target node is in a simple table block + final parentSimpleTableNode = targetNode.parentTableNode; + if (parentSimpleTableNode != null) { + return parentSimpleTableNode; + } + return targetNode; }, ); + dragToMoveNode( context, node: widget.blockComponentContext.node, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index f38d1d7257..2e56331faf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -36,6 +36,15 @@ Future dragToMoveNode( return; } + if (shouldIgnoreDragTarget( + editorState: editorState, + dragNode: node, + targetPath: acceptedPath, + )) { + Log.info('Drop ignored: node($node, ${node.path}), path($acceptedPath)'); + return; + } + final position = getDragAreaPosition(context, targetNode, dragOffset); if (position == null) { Log.info('position is null'); @@ -182,6 +191,7 @@ Future dragToMoveNode( Node dragTargetNode, Offset dragOffset, ) { + debugPrint('getDragAreaPosition - dragTargetNode: ${dragTargetNode.type}'); final selectable = dragTargetNode.selectable; final renderBox = selectable?.context.findRenderObject() as RenderBox?; if (selectable == null || renderBox == null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index a883c3410d..eaacb43ea6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -263,10 +263,13 @@ class OutlineItemWidget extends StatelessWidget { textDirection: textDirection, children: [ HSpace(node.leftIndent), - Text( - node.outlineItemText, - textDirection: textDirection, - style: style, + Flexible( + child: Text( + node.outlineItemText, + textDirection: textDirection, + style: style, + overflow: TextOverflow.ellipsis, + ), ), ], ), diff --git a/frontend/appflowy_flutter/linux/CMakeLists.txt b/frontend/appflowy_flutter/linux/CMakeLists.txt index 14ba0cb311..b9f7cce174 100644 --- a/frontend/appflowy_flutter/linux/CMakeLists.txt +++ b/frontend/appflowy_flutter/linux/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) -set(BINARY_NAME "appflowy") +set(BINARY_NAME "AppFlowy") set(APPLICATION_ID "io.appflowy.appflowy") cmake_policy(SET CMP0063 NEW) diff --git a/frontend/appflowy_flutter/linux/packaging/appimage/make_config.yaml b/frontend/appflowy_flutter/linux/packaging/appimage/make_config.yaml deleted file mode 100644 index f7dffe0d3a..0000000000 --- a/frontend/appflowy_flutter/linux/packaging/appimage/make_config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -display_name: AppFlowy - -icon: linux/packaging/assets/logo.png - -keywords: - - AppFlowy - - Office - - Document - - Database - - Note - - Kanban - - Note - -generic_name: AppFlowy - -categories: - - Office - -startup_notify: true - -supported_mime_type: - - x-scheme-handler/appflowy-flutter diff --git a/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml b/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml index ff70893ef6..801a5dbc02 100644 --- a/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml +++ b/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml @@ -29,8 +29,7 @@ essential: false section: x11 priority: optional -supported_mime_type: - - x-scheme-handler/appflowy-flutter +supportedMimeType: x-scheme-handler/appflowy-flutter dependencies: - libnotify-bin diff --git a/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml b/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml index 46fd395ad1..3fcdea03bc 100644 --- a/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml +++ b/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml @@ -26,8 +26,7 @@ categories: startup_notify: true -supported_mime_type: - - x-scheme-handler/appflowy-flutter +supportedMimeType: x-scheme-handler/appflowy-flutter requires: - libnotify diff --git a/frontend/appflowy_flutter/macos/packaging/assets/background.jpg b/frontend/appflowy_flutter/macos/packaging/assets/background.jpg deleted file mode 100644 index 57e29d8b6a2a81eda76f974e70c876d2169b38e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20532 zcmeIa2T)td)-EdNaUKR^Odf*)+aNGG!;HbkCfZ~WVL&DbAuu^gnsLA;8^2+yMX*@L$CLKe5g~a}NX`3#}jDK>^1PKQ8RE zV|>l?KXBOJ*zG@X{NFgh%0mBG=Fek%+w(tRxBrAggPsQ+%Y6A;9{fDuZ#;R7wP8NN ze?RMQ`n$xJ+GT`=o~l0I*R409>{Q0C>Lw02e_2DCj@a{YF~Ao92FM*FMZi6P0zmbM3eZ0`kN-3JJCpyj9F9{T z{RQCp;l$^YcBfA808a9pIK^|~r~@GOw?UsibsYZ>4FJxcJ@>V{dJaO_^K>NqP z;{hj5ojP^$^rT$2_!O>-^Gp2tRM9m|;FfxFgPP`j|A5dx z>T$=ZMUV46aq7%JGe7PjCr|xw;>@}87tS8X|H5+uaPs8I6DLodIeYTl52ua=kMZd< zJZFEpd5%}%hil@$KIL=u|0DUsdBy7z_buE4YHlehzxY@?MB=x6|LJG~aOpVBNuEZF~TrBv|G#*?0xc!ZEFX2fyKSORl(eEAAF*7^7KU6C@FfsEKknSHbjMlN_pmVn757gRY3qEICbu zO3lk??%>c3(rm~=j`B`09Q;f3f_Ez8#=s|2Xae0aE)6WY0Tc2rPx|ep*xhaOt|ruP zpzxl6**3~#IH7D)tfnND<*>W0t=6rqU3HH7p~U8u3{2Q@7L>_3_5S!8QSOSL zKeokS4@_Ai`uOa)9^TcvaFFwEbhho*4pL)%fvN$0p)sY?v+J4kt)49wHmAE*(6ykov22}nf<(3{q@7d=th(>}`~TIuejH+2GWq*-M5CfcnxU-&!@-TA zB0-8`O>tmlJo~oBYQ6F@JjE}3 zytsvv8GNcpX3-Pj38=AVQC-qI+{8gs_-tX4@&LMvi5t3O# z!;9obG-A?+csJJFaOOEESz^g`!DrpR751q#HGOc}oO0uo(Hl-S zg=wow>S!cf#xBRaUN=nCc$a^urEIzR4P*)DYuqwkf{*Pf)TO5hB5Fhy3BQqoyZDzg z>r;Q6;%bS0>4+1E%L(;f?rjEXY_uvJ+}Z)r_0DPYzFIfGK1z(UjHeVeY?p{Y@dJSZ z{v=L@+LyV+m5e~73f#%GM6?R4vZVrjV{8dJE6ZUK#J;i;#Vp^@S)KmSVQE!AlZQ^N z6*dGtKkfbg+J%--b~e4x@``$uY}gXWzGx}O)_|%q#cTMndYr?>(Jo)X|7tbhQjNpa zzj{rLDVGS31!o}WgXil1+|GeZApj{$~ zPnl;jAWVw@kKXrFrp0bGOU0TtyMfQL-P(=kTkpU~fq}M3qhqwG!6cBSE#Ky{XRmH< zSPeJ^i-v4xF(f9iG1y`^Y)pNJ*|OFVpk$J)qlMQyQD({S6!?et6P)JbWD&<=xlqrP zyUdT!ppD#^v-_D9wk539}>@BtQt(^^I`Imrf?iEi#!YqXE*Sx+7v9ON!6!()TSLrT|+m*Vq)<5NvpDUM&lyas5#arBuQ^D!ldN4;fLv)abO1;&3vjn*%=krkbz5AQ+Yi(b!@|TDQ6}`mi@W@Ha`b*b&>!nB`2>Zz zcSq@}E$l)y8Yd@sRC;|h+l*8A8lC!ut%_wQacg{Es8X%L+h}Z!2HEoKle0gBKW4y} zHfNkDTMenYPX4-v_*o$vIWIMkPo#5vXV6P>ntvt;obP{C4lAewE^I1*~WAi4ki7}!lC8R(p^P7>_QNQYMwO!U3 z3q}-y`1bg2SIUzEd!RBu_)C&Zi#JQcu5d~_Z#9be(i;Tis6<>&Duay*!gabj%6D;N zg_t~lDW#t(>+GF$L&TM_TA4MsbXYsemEROHD)(9f%RoV%gN$rC_dN9)3p6&v>!Y*+c!1@(UG3)8uLE}Qon*4t%1WTn=WcjD*WH+E? zva{!+fC>~)6_%21%6^CXs`x$?q!v$k&}gz=WL zS3D?Tw>V0*X}88K*Qbb2e02nof5kNXakI>NAfs<#Xbb4&W36sie$DTJwAv>`m~k$4o`j>P(n!Kb`Ufi? z8&!@LuRO?Z<+*I7)T4<04Afsxl`VbbENbBD9&!=27rQR@(DbG2pChF!IC%L6%AQ;* zGY@iJ);g1W*IUz}RJPoJss)8g(|%&;Eyj@MThKm3uW%yQokJZA- zCp+6PdfWZ))*{BQg5{87yPa#3re>^u)d?WQHhc4iA>*T3AZ7q{M)gOVS^Ju*;DtrX zqwNGu?eO!KkV}EH&V`33!X<|a0cjP_zK zYUy+_3RLG}c8xXqY(W0R>eLV6f4L;C%&A>uz5FG4R6B+lbp(hF48x_>C#_D&c$JS} z>m4T>apmPT5PvCU_c1$Q3Bi~LTekhAcU4jVmSOe#%(phf=jL~U%vt(GQNmzks8;7_ zVk4TAwZaGTZH(JXpKbOuX*P=a39RORrtE88+GVKmii(_4`NJRgy`O*=lSNDN^ADx` zp!0{1!vFXjwr%Kr*(o=VKY#|k342%Jn-ySc`ug=nnPhjTvt3$7h+Fs}diACTY(yiS z=5BnEN++h}tL(zcr+g0;LUmne!6Jdb1zHov@E&y2#!*Y31j;WBPQcW1hz014>%MzY z)Ep6c+ktKj>4>Vy-9NWEXP9=V9F|;Z;hS%^?F;%8Y?{6=*w%_1*}m(fiDSC1rHEe=RX5N<6cn^)meNM`Wz1M)1@*Tp)&48QkaFLVZ>> zfh}?#QRAZ;rBbb14j!2O9y7!Te1%V|*^6FpaW&$c-1@JMgYHR>p32=yto!Dd?7)Ak zw02C5nE-Xlc>1d;pXJKhO(=Dttadqd)OzIuaak6v5)qks4?3&5yflpRzI8S?d-Dfd zEqGQ-#_z1FiG(W$aorqty}Gkr+EhZq%4G5zb7OGe>$xrUsn(WFZ_-&@!yRl%4pti3r%)ADNBP54|e?QgQ*C*+$X8 zM75|!3-(uRY7I8v?Q){fm*XsN*-B{~0pjx2+JCEOu0S`afnp47W8dgRQL&q5Zcb2+ z_e^1^Xw}BEUeUu`wd}r!6EuN{u${FTl~a!w>%~U$#Ggq%7>_(l6}>e@TfEA}n!I$_ z5bS`NI|X-Te-7rtdBAOxVO5S_nrgtRa>2YT%!(LGjb3sSy6d8lVN0xDjBoLL>z@_C zT-Q`Gw{jcIaR!Rj<1diO>;R#f6rj|uOqr=^&{yA5QU}u|hjbxCQqD=_vAt z(rsB9@M1{2Cz*c7wC?#RHO8u5rXGKii_Xm`)>o`rz%womCF5TbQ1x{MLS> zZn-|#aVsp^+(qmOyn*CG0#$_~+z!;?1+YePBkFo>e!F79Q@<%58-Ylu$>Sd)lzT5L z85K^3UQoA`=&j0vKd=`h-->PUsQ+{5+}F*o^3U1mxUrVzjhA0^Ul#@3rRBMoj4h9P zL48+5Fy(Sp70@YJTdd!Ox<9Nzro3JsLfWDA6_b)a{ON(-S#S6zA_Zcu5HW-gZCWAU1XQ_Rm2g7q%@cz>wkzh^D7@LXqlnagK%f3{TJmB-xYr zpb(@iIr(e6fH9MAqN(BC5x``yVEVU}ecC5dL?U%sKDmA^(y_(Pr&%I8ajC~eXja0C&FWI*z%y16idyxEzC$E2p_j-J;mQeCkk3 zPDcQ*; zB6XPUVy79)ko&_G3r?McfFAl`$+wuw&X0DwTOPdxhoW7Win2x3Bk<@m@GgwsQ%!h0 z4Ehl&(DWxRS=!~p*e@AJ0CXbx%;J0JvreOWd*!`JLfcS>B|XT3p5@7Qc#NFT5x~RP z=Zoi>VR(ZsX+?PxDgP4vAqibjT=TU@9d1OYS~*MMrqd_V z3`{c`Q!+v=^1sa)fn_`nHyhIrXt`CA#QmvFyZKYa#Ok3_2uC}!jzTx8#?G@HcdLLU zKDD#lCBE|`Ax<8Hfz#eglK8jw4SpZI%v~Dz0zv2xTsLup%kTq_gum4cz6f zx^dU;i!t-Udi#8bC1lN@v!wC>b={{>_~&bx-`&nw>S~9ZiY)B)!?gTvDvXtlls27v z^7X^b5nTIKM%HIBeIKmS2|1*FLkcC2KWaULq?N8A^BYxFXf6~^vMA@INKv5FN@@Q! zvC?~moWyiki+Oe%1hF*_#Z{2L=HY15Q-20E)2Fe^!7nhkzPBU3 z%I&2TBE?*Z<8ldZ)O7WwNXqr>#O8U4kle0z+cCv0er1%j`c`wdW+mu$z_ntg4{)Y| z5NLechv*H=2~8TCh=>i=>&>3?LN zIuC!?e8`;Lehr^)49va%aITCEZu^5b|-f+ehZO|$I}(IWt7_i@0rCBZfJeT=Jq zjhpqetTfhm-D}m8a~+Y-XR$Ra8-f$N&AFl21s1P@YJ34A>)AVztYv)&ei-u-ogRsXm;!P9GH@t3iXCfExT zW$h6IpNY01Rl5DS+XD~d<<6IpPj!VKPFc9s4%Q;H-=rjd?M<3@v2=#;DA-lqwSe-D z41U~-rKRW?1{&LVvLg~UTxvtdD|(}x+r;{!uI@mEr9@07927dkN_hX;h_Vsp5Tmwd z@HAdf(!x$c{P?}u?wUGcfZ#YMzyes4A|J9AVs0}11Q!h)a4K@96QARd`8+@~Qc$=G=p^JjPY-$)Vh}A}y;WweBB<<< zn=Eed=EvSk_j<0;2ajjojbX|ph{dG8x6zNU%8YDy;Q!mFI4Pgq zTFdLg>SaDz0rTBn#oBf?!@#I4Mdu+Si)2-7v5Z|-W;7ogB`b(_jFqz&m8_g|_xVDV z{W@Fr5q-CZKlZJ9k0>r!x2HGIWnTp)t4hC9pSTd1_~DR*noXu7?qo!^9}b4qYZG!h zHL#(GpEuM4JXyYaD)Qu}=RlMza3Af%V*9N*!tYTh+vcBd zw1wt?^=*b6Gs7+m*=WeJGubxbB4unaoi#WU$8_7xnKE5^KcN8?p_{8LIjP#Z4#}>C zGWTs(I6bkEHxxa)O+ht&Qc~D2UrJ#k*2)96YE#jf(Y^kto?G``X-)*j^X)6cOLeuv zGbT!A*su=>wrr7^tV+MpW_se%H1{UQH12U#bz=%$EokA;(8oFrhWH3- za*}HP8XiJ%EA4itL3;{xt-ii`mPP=PBWKx1fR%8A(<^&t_GTj-pAQ>^3I~=+K*V*C+c_)(H3&U?j z$nK3L71e7B_HqsKiydq$iMdg{IXjkU*pzhMMz(D@zj*#Yk^iaZE3LK_FzYHOlTj6o zA^I}<$(R2BAV{@hFjI`w{nu%aI$i*-2IsVB<4sts+;?2qnjkJ6aL zCl2b$j3r<27%d%r?w#<_I3^DE;2-_cN;@@q6AmgNgdpjB~-I6I)jxRm^5SQ`jY#Ds(>}r>-n@@_e=f=kC6; zd+z7-4NCe~!TS*}L%1mnL3S0zyTysbIU8)2=0BhvPKV;kL2AL1872tFgo3KIr>^1- zj5{+`)W{`VLK~H&(ywxl;UR9F2aSo6lxBj8-&BS3f0Ju<$w)i-rYo;2`$8d5?opp+ z>w@r&ad^jv`bLhut3WUWR*yzXx}NWPo_m%t`8wlSghBx4281|`BwaOfPq)9H8@%Yq zvZ%RSPxAVlPEQ2d-T(2`ys^#mRJz8%y>o-0B|G+YmN|*qgOc<;XlDEvwynE#sM}Tf zVM-Re(*7)Zb*j|QD&rE={!P)7<@Ej@+=o1*u27;4B42+irpnvM$Mcsr%GzS3Hg9~s zwV|VWM<~f!TyT3-O9=}zaP``r_Cy(Kn5v)ck<~k$hUpD3u(J#JTuTRi7+=P2XC*;w zOV+Yq0EM_#qmRPWVDxD#>4L#W-=1haq|X&1)M{%mHG2Lt)T)A3b4CQM^KD znn8ZIJsnR(nuFja-8H2-B9AgOCW5ixpBk05zvmRsuunvADX?4@qp{MhLlPCAsu}J0 zV%RrGURWLNQGuBZf_Fb9gks8Ye)=O)tExkVCe;|QPg6pvjcN7%DDd&+S8X`It95=K z!!i~_T5U=sm$q%471%;&LHbpTF5^ScA-u$f?)mni3W#4tL@xilR)%jx<} z3(bD$*+bNul`(dikBfKX<|G1CHN`GAF5Z|@g`N)!fIIeY$8TQNd%J!c@~$}N0c^gD zUSYE8;&-`1Q7&jItCMio>dm6|wIe{)YBH%+NGGH)`~!W`^R|rk9~I-;abF8NQsmlB z;_?eH$IE4361)v|fL+eAQY#@;xkV|hNASjA?m4Gk{JR#k`gzcvWXof2LEdiUA|KZ> zECausJ)Iyt&_4plCx74Csos;f?~8IrN;B40aiX>YKi!+K?@Ub5)$(m&DuNPuve?#P z_ShW(hD%@TeRVAx!K#M2=uhTj6{?zD@Q>fyY&W#Lp}@57RRwZ~=?@3@oD9o81w!ZCjL${Y=zZ*b4!+o((tCTr+mbNLz%t7IdO4%rm*DGgi%KQ(WVV_&1G1;*|M zOd`xDhgWS4tG^O%UMbr1)l=1~s=2u{{sjJC-;E?Y`0l{adPD!s)}cUCWCJFs{~^f% z5g_r=D%SWMwIfy}`;wK-WdO z9p+Kwmm->>);T{ptADZU7m?_c|p@Dw!Ot zyx20WZbjihN(!o=v<&)o1n8VTyEFeyn7;%0W)iJob}IK1ulm{&@e4~h@ph%(dK9_P znKLxs*hkJ4a^PTDG+k#}J`8<1hG@{^0aD)M|w>eonX?HE7rFS#_#XmHo{#qkMz z+Hz+9vi0gcn*Vm?-PAtQ97h`@HZSo#+E4O(c*b>o=>6RM;;y`p;+&FxN(#fUn3Y(cSzP9b*KB*;rGc6^SXQBQ%e7#-= zg3TE@EbSsh!z^CpJ5hb}Aa`s#W+SPrA=0#P*xLr(KZ4_iG)HxI`n;CA(rpaQ#iPsK z&8ZN7K~O$IuaihOOj+lwBY&TK*tN*l9@t`fK<`_7sTw8L zP~$p}+rfx^!%6e^7CY-DP0xnQfslqO9DkoR;u)-u=5APvfYNTo4$rY#>$Y1`G8UE5 zFY~_PVVE0LiL|?2gI5Vf4n&2`JK@+G-v_lM!%>If9Y|8>)hhN_x5JFaP=!>Uj*<^p zgc;}kZpt_bQ{=4(t9>0g6HUd&FMCyR4qPd=>+k9p^`xk<%0}b;%pgZr1cn zXvUf=PRCIc`ic3R^}ZFpsB5NwT_msPLF?j`OGqW>jJI2J{VB3GLolcTY2H!Q{0MOU z;K4#5d4wn`HZVhqNo;dR7o}&_86pm2DWkD<6dNpNDkmY$9%>U+5Ssm6 zb&Ip^P6kcJ29B2s#m%Z}UIH7Aw;HI*RCQ(iWNb!E5upf#V{usNo8{*3QHF1d zrG1z9MR>DTTKBsiQTbAHjGg^@aUzUQNJTzJIP z!&~>`Fhisf8x^bdNHWThP}rX#G8*m}>2h0TW=xFrysvh#K#1Z`|@7&u)o~nyCHFkk;vlB2^09Ca7GGZ}Bm{7`wDM3(ezkJ0xhig5q&3iDsoo6AuH{sNr>pV&7i>Qrl z=CvUAaKVYV1_>K|P}QAZ>(q+3Nt;XkO9@yt(he;S7qm(j6@IJI+T7|qvbx_^Cklmn zd=h*BSH+ht2|C8aYaX1#sBq1s$Se9Aka-`AiefXjC*Ev!=kegmlGJbnXgD-Pa>af= zB^RbPbOcyhoHIvxQ<;Q=@jZdFd6%vZ@-&5HUT7uaJ5N>G1FKS*HAp0GMLG;&%cm@I)PBl}yP{1!=gpvdeAa(5TPJ0d@6Tg* ze|=crPBdvND8k;4+q7S1>nmy&2$yhcNvnA@le-_f_%-MPB+#=}WlzVUYdd3EC|ERO zc@}K=p?{ew`27grAB&DY?Q=irw6t{S)~9u7@lN2!FuvhNl`BU8y1c@u7giTuvXZ_J z#C#CAm%nKHy=qeWPbSssQR0`xyzCVBKYUByFOWUVR}|-dd9JcJ))q@JjWaEx z8tMIf&`VYc=M{iQ6v7%3Ct9z_Pkk=>R6Yg_BW;YsOrx@2a4U_=c=%HBz+Sz?C)FrF zbEJ{jIru%cjA>-Y;R7+pF^-r22R|oIXbti?Gl$9JY!WTLtd$r0V`J*d7g*M#(O^4G zTJFe#<+Fj7srR(>@6K1XqNM4%9}a$2h@m1*aHYeeX+tTXkE}ESF)K{9sWy{XvNp)p{({ z2;CbpwXd^~RpY1Rj~V)y;xKAoI}~)q|4D*5s4w9KE&EdJ9UBuz*uK&l=uw=Uu;%*G zUTK8(P6=bPXv0*mJ1!qzO6XKgNOB))xFS;nXTK)01&b{<8Wq_un(5zXTI?gllD z7Va!R?X2UHYzqT)>Uf@N3a@uxJ*|CfRy0>sCUA{+#~|;+A-E4&zHB$PCs`y?yn+Pd zVEmx;Pu0G*sSbxF#+sijt6btqtlM`#{#y9b-mb&rBLy3OwwPdjuH3MglI)!0P;zm$ zwK;D0@z|cZLz&~-u17$h1_K$%J&D~52}Pd45*x#obA7)N` zYhg>VO7#?Ow68LAOaroO;zTH*Bq?{eKU*;{j!CVPI)^ZlsPZ9`)GZG+kz-^dx4jMe9=Q@IL#&&MHg$*h6M z>laPvoqf-9(t3;PqJnXb4*O$EFH?t!D7Pk|i8_=d`b@JQ$#75>cTJ~s*R%QSQe#S5 z^Ml%erpp+Q06G5n53g2&BcroyCSMiI<#r#&gN&Rl2t!48dcF6eM8Q!CstqEEyTzJu zB#$j3?=CxWY=c_Hm6k_l`1WA+y5t8o*8_{@P-hN2-!H)VGUvivfOCy5 z(UUXUFMY?{Hm}EDTdZ*q*HImWFov{h4r~{{Hcm^?-xWJGoQvD+H_uthky-6P?Dib5 zR9BK^2=I1)@LzRSrK3LLAC%#rj z)xFkLIpop1)L2OfTWq@>ZkW7SY~LpQ^e_Kh7m;gkD~FXBw#Z1;0V5g^lH*GONdj+d zcsq!D908CRPWCvZ--PhpP^gYCX&*fR_LI6(Z|?7BcG(n?nY1kr9fotCD(Gk<0+-lD zULvRLzhC$cf-O&Ztm^*Nq49)47l*|gDv2u=8yP**EhgAT)841juY57Gp@~Aw;3IR# zJD#eK0Iv`9Hl_tO&@S?e+k_!searV>XAfsOzcZ+6VxwFB&SQ}G{*0Mk6U!)d$m6!V zW`Zu)9OOZVE*$iRxeUu0+DRBPyO=;(Xk2b_cFNTf!ecOHHFllZ_gt|x;P{l6qe<5$ zJTlQLE%X-KA;lj&=zPch=?%sn^=KuOeD!qz?PksP#VTI@Om^{Jg%F=q+&Or%|IJn{ zj}fGM2o5n=p>{d9p;j)G`Bd@#dgEL0)--=D(={EDcPMPLhuCrde4tY@Egi(5N$4XD z`6FxpOsX&&dm7oaOnpE1<h_Co9cQ4CvNOk|uKVK!6d=0f^ zr;HyU#zS0g;p8r~UDW^j{i8v_LU8K|p}T7PmyJFxDLSL@jI2s)k6@b#SaD8Hj9oVN zj*t7RqOaL&HxV~p^1D(gcYFxqKM08OnorH^O=9Z;YbRlXhj@wxZLwc|O*t;;tFOwn zoPlO=<-{0+5*MR!ZU6M6>wbi%9;Ftf3jsIYl@89d;QkzGCL#-@Kg$_Tdo?9lZd^p~ zp6|u#ypU^u{F#$r{&bX~dQPUW*yMsUs-E;EOCo?wo)zNGevEzdu#@V=y@fwtiExkl zbJAHmY@J^7lfe$sU9CR?fKtjhmBf#AVrve1>f0#ONE9R{n``~x+S2tPHs_}?1JC&`eI}%( z0!Y8O-6L!`C?Dg`SO9_*ZZuLeKwc;BaHzGorC+g|VrdCv$}z!=yIm&w5hZ zni;w=cJe-%{&QG2T&o+2XWID)@HmmXm&u6OX0|n>D#$>&-~SKV>|BCt^{}-o5>po= z;>GF7+;0rAxmP)_J|M2NeXBxrXFw?xn+vAu?sh4>=jcR@N9=}Syii3=jJ9bOwfqPW zoSSpQKXg-ZxK7Mrd#d#a@C_4k*kiiFu%cKE)cmSBPe9h@@ZH>5Prhl_W%-&e>v>Q} zztgjMbI0fqIb&Xzl_scC_y8AjCmKPJlLCfgHX0~fTb=aXeQ`53Mh6O=R)*_7+pBog zuy&wlP8sz+0vt9~LP2oGz1HmA?>r+#l08iain+}q-h)%#`tQh-E~`eC(Y33xI`1kE z+V1X3t57{HPqWc48XCxobE^^caH*QG9HeN87=c76&t}3F6FGqcn%iq}Vo#@#>h)wd zLOUUAe{WBFD)!??rBR31*VOxaaqp&AEo$P8suM1!)pKmHt-1JkjaPV}>4$}s=vntW z(912kv#V`Bddqrz=uF^=cb9$On%P&P(NmkA2?MIdSLmgA{$Qq% zySSNq7ywhPr;%G*S0YW(dghTPeftRKjVY*I5!7hk1{>RfV00KDy#iCq|M%_UREgUp z#r3f-PXDPiuyR)UO?LZA{bHL=YVqdIV4DspzD!Q?!oc<7{LcZyOrmleIlu)E;gIuf_P(D2gMT4|mM0K3C_0MW^HE)$_Jn9YU`S7ZT?+fY(t|E8fn?_y>gD zU-yegn{rlZ(u5)zmBdzpCJjZaqikj%8c@v9wQ}PGJ+F;6`vI6uU-6;w5g;MlT~?VC zI{@F@2)|MxU}GJB&{`K4uEtTck>Pc;PC5cunR=G)8CHxciS2suD}0C28_M0rJtB_) zt;dIc^z4}=9;JiGy+)rHLMu}NeL9yt$_|JJ==SrfP7!0 zhYCKnmxxsf_I76QOlzcN+~oqFYJ+DULVXID>TT}0yyPRx8=e!!tmDFRZdiamE8Uzd zzMszgZX^ulbH3-k8_|@xKw`#$+=h0Vw&NlQmtjkoa;eBI;tWGZN#QO;gB8`E*OF|v z6*G@rm%!9bZm0gDLqDjzLy-bVMSALRAwNS%a25Dyt4* z=*a0n+2AM=O|vg@IG@GKt03mk=~l-hGOV@g)*B03`Rjk*L;;r;Z*&Fb(r9QO%@ zc<}B(;9=%p*K}w{fCm;C4z82Ot?zQ_K|-1z{f7aFQ*>xqfEGWf;f%Its z>EFH%W{^~qG7`ZAr=~2%@d2+Af5;k@xs-$~zYaUN(Hu)%m`n01(#_kZwC-gST~JPa zh$M-7+d!3dQo2_qJMsdL+E30R>wUnebAYTS5yBCOMOQs?dW{kQki&KP}0@WUe_ z@^|QK8U^cp(^L@;-8z@{k^PR~5$c3z1$#OKE|h~9mtGz7WEt4fI7vOgo#rG+{{9xx zUxb8peX4JYHn3d+K`NC=M&@*}6#S!%*&;maZ^RF88PcQgry`1V>llQmD`F+} zhAt1Tl+=7(tr4k2w|c~M%>nz0?5qom%I@gEO&f=A&AgsTYwFbtm4`P-sgji7 zdUfl2C1vA-ympD)8Vhj`4=Hgs4`~u3aaL+7A~M!w;%Wsa+Rr3Za7+rQL}>#JKIKtR z;^k3bZ0(2gt&NV*#N`TLxL(-t@+hSEUHSy1zUVS;g7?)2+}B*!+xL0nCGVKsOD(z4 zb=HT<|sHHl<2mscfw&}LO^<drMgCLi^yG+)h+8XLQmI*>B%We8gu`C3>{l1Ptc=@dO-+w9a z53(Rf7UQk3Sn8dqA;8b-9Ipen6KNxEB8yS6p?T>FMS%75bg%5{AOGu)`VP zrAbRB+pvm84qF2`=;*MS-F}dyLbeErCe0j~UeYD)26{fMsT;7Z^&#_GEIe=cxqZOy z?HgXy%6?d&40vYcmFND?)s>#}ySttXmfOo=1!$O0*n&i9H?lK4W^Y@8$M=u`n$Qb% z?nlh0$ZprwrQ9;vRxX(-G(jZm5M9iRWc1iJd_jEIzTby_GjxTR@n(s^?xrr*A~bs6 zTgNwHG8Q$6y+3pXU}7!f7f+kuH$Ad)vw&V%YsWKG;2ql~F%p565qDNtfhS1>svr{4 z6mvW*6oPy1vG?A|i$* zsw2e3Nn2qD>9QmUcZwL?TA_0&*rz(ko_w{fGcy{)VV2<@R}%eoS^InW#cB0$`Fl-I zZ+8A6kokPfSJ{Ji9g?2Hy05U7=Bc+@1oXC;bf`;foK2<2T8tEVLR(kflOiWzQ1d_~ zoxVetVTlQeB_1B05&fVdnZz0+8yN@UQyvim1d{AY^JFS1DNXj)T9p37QvXZgH5e!B z;p*PnIihs{2D9TB736>b+yTi+4epM!YeZYKm@28Da5_ zC8gOecm^TYmf#h`m6B?s4T*ja-(oGGvg~v13-ECyYI9_;DwF1+x!2LzdRVt|^)iMf z_`1F>{ZF5olvPY~1|=&2hgvZX_g}o(GE3QUX>-UWAk%f%ST7C^2L_dN=Ex28!AnU7 zefxcEyA}gO67$hsB-}=Vlla5iE&6yR63ak28blg|D%oeDFo>MP-4Oa#tY1?m*ty%YQ9?pRSkdDc0qGIQQ(`adQRDjZxaSxsKoc;RMvkV|SF_@z_1ieE)bFR~w0U z@}DE@`qQR2hRH{OLZi56L^Ez^QN-RJn&)_SIZROvZgY42jU2c_V|%bAq_EeJt~;F} zes4qJ2q1ere)8WXw&_XkEx{bL;#HG&i|bKKkv2BK<_dd8pbc_9&RGu)Rp8QO6Z6ou z3r{9wi%O2Sb}kTOhe3n5z@#q8$MI{6H@1n*VF#X~hvVB_^0x8g^`T0MDSHkir6a&c zT+(WtuepPCX?zBkU1@-1U_JNMYZ{kT8wa;I6_Q?@fv0laNcY#^9U@M#w68*aaPyU^ z4V*v{J=~$JM(hw+LRK*cOK~g^n+sH!gliR;w=O?B{)Yhu4ZMMKX}3RQ$82-uW`h&6 ze23pPlzUm`Mp;EWaf16i^?Jm>+lCH3@K!Dr{u&wDu(%m-eVzV%&|e^Ma9%aZ>rg@O zuJkT>`hk+Ov23tX+_`xL-Zj15f`vZhje7G&il!r(l$Yd+s|{BtQjIN?(#N)m+eLC_ zCY|{B#T^S!($3-2+?YAHlp}yAr87~QspJxg$RF7crNq%xrDMM%V904pgY&HTaBll-68Y!&)ha1$+(b(8# z_8xm!GEk+2(s-c4Adt1VOPTCyH|9`mvclT+>`u%ylhF`zm_qr!9e42?K>gXnBw!jb zy!1vs7B4-h*V^+j?`$$xz-&b4wn*F0Fu~JNGSKNuM|Je^qp+>!;Kr zbOJ;z@$a$<%2*}PYr6YrF;!$BQ7D^XShFF zRE&)8_dqrQ>w{rRO0nXeT&i=8*k|z}vGF-%wDO%mxf;a=;H-#KA_%&JP$V=>G504YD2#Zi4;L}`RtLRH#OdsrVteXzvK<@(DBe@_u3)^nToY?M$v78_K*rU2jCjI;(R~8%A>xo-2IR@&1yBS7}S;;|>1 zE7ve#tz2v8*?Zu31Q;vfIG^9n%_>G}uq}^IVSG1S)U3F(gTG?XvJV#1;C)u)a=`tF z7ZBY&0=TSjjsQNtHTW8OBQnr>!I0x+v8n5*RlIOWB^&}p_D3Gq<)9zgVE#}wt>S@g zcez*3Ac0Vyrl+8LNSj_a4&T~G_EM?Sn=5O(Kx(M>(8xkPvX`&$2;g8-i+2J#aPN_k z3pp-FfT(c`#dd|E=z?0(W%>>*{p+y8PM6*f4}Z! - + + @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop index 40b241e05d..9076493bb8 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Type=Application Name=AppFlowy -Icon=io.appflowy.appflowy -Exec=appflowy %U +Icon=io.appflowy.AppFlowy +Exec=AppFlowy %U Categories=Network;Productivity; Keywords=Notes DBusActivatable=true diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml index ea3b476004..c9a58b68fa 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.metainfo.xml @@ -1,46 +1,38 @@ - io.appflowy.appflowy - + io.appflowy.AppFlowy + AppFlowy -

Open Source Notion Alternative - + Open Source Notion Alternative + CC-BY-4.0 AGPL-3.0-only - +

- # Built for teams that need more control and flexibility ## 100% data control You can host - AppFlowy wherever you want; no vendor lock-in. + # Built for teams that need more control and flexibility ## 100% data control You can host AppFlowy wherever you want; no vendor lock-in.

## Unlimited customizations Design and modify AppFlowy your way with an open core codebase.

- ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What - does this mean? Faster development, better native experience, and more reliable performance. + ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance.

- # Built for individuals who care about data security and mobile experience ## 100% control of - your data Download and install AppFlowy on your local machine. You own and control your - personal data. + # Built for individuals who care about data security and mobile experience ## 100% control of your data Download and install AppFlowy on your local machine. You own and control your personal data.

- ## Extensively extensible For those with no coding experience, AppFlowy enables you to create - apps that suit your needs. It's built on a community-driven toolbox, including templates, - plugins, themes, and more. + ## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It's built on a community-driven toolbox, including templates, plugins, themes, and more.

- ## Truly native experience Faster, more stable with support for offline mode. It's also - better integrated with different devices. Moreover, AppFlowy enables users to access features - and possibilities not available on the web. + ## Truly native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web.

- - io.appflowy.appflowy.desktop + + io.appflowy.AppFlowy.desktop https://github.com/AppFlowy-IO/appflowy/raw/main/doc/imgs/welcome.png - \ No newline at end of file + diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service index fed8eabcf1..31e32415d0 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service +++ b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.service @@ -1,3 +1,3 @@ [D-BUS Service] -Name=io.appflowy.appflowy -Exec=appflowy +Name=io.appflowy.AppFlowy +Exec=AppFlowy diff --git a/frontend/scripts/flatpack-buildfiles/launcher.sh b/frontend/scripts/flatpack-buildfiles/launcher.sh index 5cda51d0a9..24b4fdbea4 100644 --- a/frontend/scripts/flatpack-buildfiles/launcher.sh +++ b/frontend/scripts/flatpack-buildfiles/launcher.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -gdbus call --session --dest io.appflowy.appflowy \ +gdbus call --session --dest io.appflowy.AppFlowy \ --object-path /io/appflowy/AppFlowy/Object \ - --method io.appflowy.appflowy.Open "['$1']" {} + --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/flutter_release_build/build_linux.sh b/frontend/scripts/flutter_release_build/build_linux.sh deleted file mode 100755 index 21f591193a..0000000000 --- a/frontend/scripts/flutter_release_build/build_linux.sh +++ /dev/null @@ -1,243 +0,0 @@ -#!/bin/bash -# This Script is used to build the AppFlowy linux zip, deb, rpm or appimage -# -# Usage: ./scripts/flutter_release_build/build_linux.sh --build_type --build_arch --version [--skip-code-generation] [--skip-rebuild-core] -# -# Options: -# -h, --help Show this help message and exit -# --build_type The type of package to build. Must be one of: -# - all: Build all package types -# - zip: Build only zip package -# - tar.xz: Build only tar.xz package -# - deb: Build only deb package -# - rpm: Build only rpm package -# - appimage: Build only appimage package -# --build_arch The architecture to build. Must be one of: -# - x86_64: Build for x86_64 architecture -# - arm64: Build for arm64 architecture (not supported yet) -# --version The version number (e.g. 0.8.2) -# --skip-code-generation Skip the code generation step -# --skip-rebuild-core Skip the core rebuild step - -show_help() { - echo "Usage: ./scripts/flutter_release_build/build_linux.sh --build_type --build_arch --version [--skip-code-generation] [--skip-rebuild-core]" - echo "" - echo "Options:" - echo " -h, --help Show this help message and exit" - echo "" - echo "Arguments:" - echo " --build_type The type of package to build. Must be one of:" - echo " - all: Build all package types" - echo " - zip: Build only zip package" - echo " - tar.xz: Build only tar.xz package" - echo " - deb: Build only deb package" - echo " - rpm: Build only rpm package" - echo " Please install the \033[33mrpm-build\033[0m and \033[33mpatchelf\033[0m before building the rpm and appimage package." - echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/rpm/." - echo " - appimage: Build only appimage package" - echo " Please install the \033[33mlocate\033[0m and \033[33mappimagetool\033[0m before building the appimage package." - echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/appimage/." - echo " --build_arch The architecture to build. Must be one of:" - echo " - x86_64: Build for x86_64 architecture" - echo " - arm64: Build for arm64 architecture (not supported yet)" - echo " --version The version number (e.g. 0.8.2)" - echo " --skip-code-generation Skip the code generation step. It may save time if you have already generated the code." - echo " --skip-rebuild-core Skip the core rebuild step. It may save time if you have already built the core." - exit 0 -} - -# Check for help flag -if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - show_help -fi - -# Parse named arguments -while [ $# -gt 0 ]; do - case "$1" in - --build_type) - BUILD_TYPE="$2" - shift 2 - ;; - --build_arch) - BUILD_ARCH="$2" - shift 2 - ;; - --version) - VERSION="$2" - shift 2 - ;; - --skip-code-generation) - SKIP_CODE_GENERATION=true - shift - ;; - --skip-rebuild-core) - SKIP_REBUILD_CORE=true - shift - ;; - *) - echo "Unknown parameter: $1" - show_help - exit 1 - ;; - esac -done - -clear_cache() { - echo -e "Clearing the cache..." - rm -rf appflowy_flutter/build/$VERSION/ -} - -info() { - echo -e "🚀 \033[32m$1\033[0m" -} - -error() { - echo -e "🚨 \033[31m$1\033[0m" -} - -# Validate build type argument -if [ -z "$BUILD_TYPE" ]; then - error "Please specify build type with --build_type: all, zip, tar.xz, deb, rpm, appimage" - exit 1 -fi - -# Validate version argument -if [ -z "$VERSION" ]; then - error "Please specify version number with --version (e.g. 0.8.2)" - exit 1 -fi - -# Validate build arch argument -if [ -z "$BUILD_ARCH" ]; then - error "Please specify build arch with --build_arch: x86_64, arm64 or universal" - exit 1 -fi - -if [ "$BUILD_TYPE" != "all" ] && [ "$BUILD_TYPE" != "zip" ] && [ "$BUILD_TYPE" != "tar.xz" ] && [ "$BUILD_TYPE" != "deb" ] && [ "$BUILD_TYPE" != "rpm" ] && [ "$BUILD_TYPE" != "appimage" ]; then - error "Invalid build type. Must be one of: all, zip, tar.xz, deb, rpm, appimage" - exit 1 -fi - -has_built_core=false -has_generated_code=false - -prepare_build() { - info "Preparing build..." - - # Build the rust-lib with version - if [ "$SKIP_REBUILD_CORE" != "true" ] && [ "$has_built_core" != "true" ]; then - cargo make --env APP_VERSION=$VERSION --profile production-linux-$BUILD_ARCH appflowy-core-release - has_built_core=true - fi - - if [ "$SKIP_CODE_GENERATION" != "true" ] && [ "$has_generated_code" != "true" ]; then - cargo make --env APP_VERSION=$VERSION --profile production-linux-$BUILD_ARCH code_generation - has_generated_code=true - fi -} - -build_zip() { - info "Building zip package version $VERSION..." - - prepare_build - - cd appflowy_flutter - flutter_distributor release --name=prod --jobs=release-prod-linux-zip --skip-clean - cd .. - mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.zip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip - - info "Zip package built successfully. The zip package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip" -} - -build_deb() { - info "Building deb package version $VERSION..." - - prepare_build - - cd appflowy_flutter - flutter_distributor release --name=prod --jobs=release-prod-linux-deb --skip-clean - cd .. - mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.deb appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.deb - - info "Deb package built successfully. The deb package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.deb" -} - -build_rpm() { - info "Building rpm package version $VERSION..." - - prepare_build - - cd appflowy_flutter - flutter_distributor release --name=prod --jobs=release-prod-linux-rpm --skip-clean - cd .. - mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.rpm appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.rpm - - info "RPM package built successfully. The RPM package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.rpm" -} - -# Function to build AppImage package -build_appimage() { - info "Building AppImage package version $VERSION..." - - prepare_build - - cd appflowy_flutter - flutter_distributor release --name=prod --jobs=release-prod-linux-appimage --skip-clean - cd .. - mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-linux.AppImage appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.AppImage - - info "AppImage package built successfully. The AppImage package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.AppImage" -} - -build_tar_xz() { - info "Building tar.xz package version $VERSION..." - - prepare_build - - # step 1: check if the linux zip package is built, if not, build the zip package - if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip" ]; then - info "Linux zip package is not built. Building the zip package..." - build_zip - fi - - # step 2: unzip the zip package - unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.zip -d appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64 - - # check if the AppFlowy directory exists - if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64" ]; then - error "AppFlowy directory doesn't exist. Please check the zip package." - exit 1 - fi - - # step 3: build the tar.xz package - tar -cJvf appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.tar.xz appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64/* - - info "Tar.xz package built successfully. The tar.xz package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-linux-x86_64.tar.xz" -} - -clear_cache - -# Build packages based on build type -case $BUILD_TYPE in -"all") - build_zip - build_deb - build_rpm - build_appimage - ;; -"zip") - build_zip - ;; -"deb") - build_deb - ;; -"rpm") - build_rpm - ;; -"appimage") - build_appimage - ;; -"tar.xz") - build_tar_xz - ;; -esac diff --git a/frontend/scripts/flutter_release_build/build_macos.sh b/frontend/scripts/flutter_release_build/build_macos.sh deleted file mode 100755 index 82071bef38..0000000000 --- a/frontend/scripts/flutter_release_build/build_macos.sh +++ /dev/null @@ -1,297 +0,0 @@ -# This Script is used to build the AppFlowy macOS zip, dmg or pkg -# -# Usage: ./scripts/flutter_release_build/build_macos.sh --build_type --build_arch --version --apple-id --team-id --password [--skip-code-generation] [--skip-rebuild-core] -# -# Options: -# -h, --help Show this help message and exit -# --build_type The type of package to build. Must be one of: -# - all: Build all package types -# - zip: Build only zip package -# - dmg: Build only dmg package -# - tar.xz: Build only tar.xz package -# --build_arch The architecture to build. Must be one of: -# - x86_64: Build for x86_64 architecture -# - arm64: Build for arm64 architecture -# - universal: Build for universal architecture -# --version The version number (e.g. 0.8.2) -# --skip-code-generation Skip the code generation step -# --skip-rebuild-core Skip the core rebuild step -# --apple-id The apple id to use for the notary service -# --team-id The team id to use for the notary service -# --password The password to use for the notary service - -show_help() { - echo "Usage: ./scripts/flutter_release_build/build_macos.sh --build_type --build_arch --version --apple-id --team-id --password [--skip-code-generation] [--skip-rebuild-core]" - echo "" - echo "Options:" - echo " -h, --help Show this help message and exit" - echo "" - echo "Arguments:" - echo " --build_type The type of package to build. Must be one of:" - echo " - all: Build all package types" - echo " - zip: Build only zip package" - echo " Please install the \033[33mp7zip\033[0m before building the zip package." - echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/zip/." - echo " - tar.xz: Build only tar.xz package" - echo " - dmg: Build only dmg package" - echo " Please install the \033[33mappdmg\033[0m before building the dmg package." - echo " For more information, please refer to the https://distributor.leanflutter.dev/makers/dmg/." - echo " --build_arch The architecture to build. Must be one of:" - echo " - x86_64: Build for x86_64 architecture" - echo " - arm64: Build for arm64 architecture" - echo " - universal: Build for universal architecture" - echo " --version The version number (e.g. 0.8.2)" - echo " --skip-code-generation Skip the code generation step. It may save time if you have already generated the code." - echo " --skip-rebuild-core Skip the core rebuild step. It may save time if you have already built the core." - echo " --apple-id The apple id to use for the notary service" - echo " --team-id The team id to use for the notary service" - echo " --password The password to use for the notary service" - exit 0 -} - -# Check for help flag -if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - show_help -fi - -# Parse named arguments -while [ $# -gt 0 ]; do - case "$1" in - --build_type) - BUILD_TYPE="$2" - shift 2 - ;; - --build_arch) - BUILD_ARCH="$2" - shift 2 - ;; - --version) - VERSION="$2" - shift 2 - ;; - --skip-code-generation) - SKIP_CODE_GENERATION=true - shift - ;; - --skip-rebuild-core) - SKIP_REBUILD_CORE=true - shift - ;; - --apple-id) - APPLE_ID="$2" - shift 2 - ;; - --team-id) - TEAM_ID="$2" - shift 2 - ;; - --password) - PASSWORD="$2" - shift 2 - ;; - *) - echo "Unknown parameter: $1" - show_help - exit 1 - ;; - esac -done - -clear_cache() { - echo "Clearing the cache..." - rm -rf appflowy_flutter/build/$VERSION/ -} - -info() { - echo "🚀 \033[32m$1\033[0m" -} - -error() { - echo "🚨 \033[31m$1\033[0m" -} - -# Validate build type argument -if [ -z "$BUILD_TYPE" ]; then - error "Please specify build type with --build_type: all, zip, dmg, tar.xz" - exit 1 -fi - -# Validate version argument -if [ -z "$VERSION" ]; then - error "Please specify version number with --version (e.g. 0.8.2)" - exit 1 -fi - -# Validate build arch argument -if [ -z "$BUILD_ARCH" ]; then - error "Please specify build arch with --build_arch: x86_64, arm64 or universal" - exit 1 -fi - -if [ "$BUILD_TYPE" != "all" ] && [ "$BUILD_TYPE" != "zip" ] && [ "$BUILD_TYPE" != "dmg" ] && [ "$BUILD_TYPE" != "tar.xz" ]; then - error "Invalid build type. Must be one of: all, zip, dmg, tar.xz" - exit 1 -fi - -prepare_build() { - info "Preparing build..." - - # step 1: build the appflowy-core (rust-lib) based on the build arch - if [ "$SKIP_REBUILD_CORE" != "true" ]; then - if [ "$BUILD_ARCH" = "x86_64" ] || [ "$BUILD_ARCH" = "universal" ]; then - info "Building appflowy-core for x86_64...(This may take a while)" - cargo make --profile production-mac-x86_64 appflowy-core-release - fi - - if [ "$BUILD_ARCH" = "arm64" ] || [ "$BUILD_ARCH" = "universal" ]; then - info "Building appflowy-core for arm64...(This may take a while)" - cargo make --profile production-mac-arm64 appflowy-core-release - fi - - # step 2 (optional): combine these two libdart_ffi.a into one libdart_ffi.a if the build arch is universal - if [ "$BUILD_ARCH" = "universal" ]; then - info "Combining libdart_ffi.a for universal..." - lipo -create \ - rust-lib/target/x86_64-apple-darwin/release/libdart_ffi.a \ - rust-lib/target/aarch64-apple-darwin/release/libdart_ffi.a \ - -output rust-lib/target/libdart_ffi.a - - info "Checking the libdart_ffi.a for universal..." - lipo -archs rust-lib/target/libdart_ffi.a - - cp -rf rust-lib/target/libdart_ffi.a \ - appflowy_flutter/packages/appflowy_backend/macos/ - fi - fi - - # step 3 (optional): generate the flutter code: languages, icons and freezed files. - if [ "$SKIP_CODE_GENERATION" != "true" ]; then - info "Generating the flutter code...(This may take a while)" - cargo make code_generation - fi - - # step 4: build the zip package - info "Building the zip package..." - cd appflowy_flutter - flutter_distributor release --name=prod --jobs=release-prod-macos-zip --skip-clean - cd .. -} - -build_zip() { - info "Building zip package version $VERSION..." - - # step 1: check if the macos zip package is built, if not, build the zip package - if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then - info "macOS zip package is not built. Building the zip package..." - prepare_build - - # step 1.1: move the zip package to the build directory - mv appflowy_flutter/build/$VERSION/appflowy-$VERSION+$VERSION-macos.zip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip - fi - - # step 2: unzip the zip package and codesign the app - unzip -o appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip - - # step 3: codesign the app - # note: You must install the certificate to the system before codesigning - sudo /usr/bin/codesign --force --options runtime --deep --sign "Developer ID Application: APPFLOWY PTE. LTD" --deep --verbose AppFlowy.app -v - - # step 4: zip the app again - 7z a appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip AppFlowy.app - - info "Zip package built successfully" -} - -build_dmg() { - info "Building DMG package version $VERSION..." - - # step 1: check if the macos zip package is built, if not, build the zip package - if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then - info "macOS zip package is not built. Building the zip package..." - build_zip - fi - - # step 2: unzip the zip package and copy the make_config.json file to the build directory - unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip -d appflowy_flutter/build/$VERSION/ - cp appflowy_flutter/macos/packaging/dmg/make_config.json appflowy_flutter/build/$VERSION/ - - # check if the AppFlowy.app doesn't exist, exit the script - if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy.app" ]; then - error "AppFlowy.app doesn't exist. Please check the zip package." - exit 1 - fi - - # check if the appdmg has been installed - if ! command -v appdmg &>/dev/null; then - info "appdmg is not installed. Installing appdmg..." - npm install -g appdmg - fi - - # step 3: build the dmg package using appdmg - # note: You must install the appdmg to the system before building the dmg package - appdmg appflowy_flutter/build/$VERSION/make_config.json appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg - - # step 4: clear the temp files - rm -rf appflowy_flutter/build/$VERSION/AppFlowy.app - rm -rf appflowy_flutter/build/$VERSION/make_config.json - - # check if the dmg package is built - if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg" ]; then - error "DMG package is not built. Please check the build process." - exit 1 - fi - - info "DMG package built successfully. The dmg package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg" - - if [ -z "$APPLE_ID" ] || [ -z "$TEAM_ID" ] || [ -z "$PASSWORD" ]; then - error "The apple id, team id and password are not specified. Please notarize the dmg package manually." - error "xcrun notarytool submit appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg --apple-id --team-id --password -v -f \"json\" --wait" - else - xcrun notarytool submit appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.dmg --apple-id $APPLE_ID --team-id $TEAM_ID --password $PASSWORD -v -f "json" --wait - info "Notarization is completed. Please check the notarization status" - fi -} - -build_tar_xz() { - info "Building tar.xz package version $VERSION..." - - # step 1: check if the macos zip package is built, if not, build the zip package - if [ ! -f "appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip" ]; then - info "macOS zip package is not built. Building the zip package..." - build_zip - fi - - # step 2: unzip the zip package and copy the make_config.json file to the build directory - unzip appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.zip -d appflowy_flutter/build/$VERSION/ - - # check if the AppFlowy.app doesn't exist, exit the script - if [ ! -d "appflowy_flutter/build/$VERSION/AppFlowy.app" ]; then - error "AppFlowy.app doesn't exist. Please check the zip package." - exit 1 - fi - - # step 3: build the tar.xz package - tar -cJvf appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.tar.xz appflowy_flutter/build/$VERSION/AppFlowy.app - - info "Tar.xz package built successfully. The tar.xz package is located at appflowy_flutter/build/$VERSION/AppFlowy-$VERSION-macos-$BUILD_ARCH.tar.xz" -} - -clear_cache - -# Build packages based on build type -case $BUILD_TYPE in -"all") - build_zip - build_dmg - build_tar_xz - ;; -"zip") - build_zip - ;; -"dmg") - build_dmg - ;; -"tar.xz") - build_tar_xz - ;; -esac diff --git a/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml index 45d52d6044..cd8103df9e 100644 --- a/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml +++ b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml @@ -11,11 +11,11 @@ script: AppDir: path: ./AppDir app_info: - id: io.appflowy.appflowy + id: io.appflowy.AppFlowy name: AppFlowy icon: appflowy.svg version: [CHANGE_THIS] - exec: appflowy + exec: AppFlowy exec_args: $@ apt: arch: diff --git a/frontend/scripts/linux_distribution/deb/AppFlowy.desktop b/frontend/scripts/linux_distribution/deb/AppFlowy.desktop index 6b485fc50e..e6851f9f42 100644 --- a/frontend/scripts/linux_distribution/deb/AppFlowy.desktop +++ b/frontend/scripts/linux_distribution/deb/AppFlowy.desktop @@ -2,7 +2,7 @@ Type=Application Name=AppFlowy Icon=/usr/share/icons/hicolor/scalable/apps/appflowy.svg -Exec=/usr/bin/appflowy %U +Exec=/usr/bin/AppFlowy %U Categories=Network;Productivity; Keywords=Notes Terminal=false diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst index fbc5b1fc91..bf2f79fa97 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst @@ -1,8 +1,8 @@ #!/usr/bin/env bash -if [ -e /usr/bin/appflowy ]; then +if [ -e /usr/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else echo "Creating Symlink in /usr/bin/appflowy" - ln -s /usr/lib/appflowy/appflowy /usr/bin/appflowy + ln -s /usr/lib/AppFlowy/AppFlowy /usr/bin/AppFlowy ln -s /usr/lib/AppFlowy/launcher.sh /usr/bin/AppFlowyLauncher.sh fi diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm index f0131be6f6..59a680e767 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm @@ -1,5 +1,5 @@ #!/usr/bin/env bash -if [ -e /usr/bin/appflowy ]; then - rm /usr/bin/appflowy +if [ -e /usr/bin/AppFlowy ]; then + rm /usr/bin/AppFlowy rm /usr/bin/AppFlowyLauncher.sh fi diff --git a/frontend/scripts/linux_distribution/deb/build_deb.sh b/frontend/scripts/linux_distribution/deb/build_deb.sh index a5fb5bc53a..42fbf7346d 100644 --- a/frontend/scripts/linux_distribution/deb/build_deb.sh +++ b/frontend/scripts/linux_distribution/deb/build_deb.sh @@ -25,9 +25,9 @@ chmod 0755 $DEBIAN/postinst chmod 0755 $DEBIAN/postrm grep -rl "\[CHANGE_THIS\]" $DEBIAN/control | xargs sed -i "s/\[CHANGE_THIS\]/$VERSION/" -cp -fR $LINUX_RELEASE_PRODUCTION/appflowy $LIB -cp ./scripts/linux_distribution/deb/appflowy.desktop $APPLICATIONS -cp ./scripts/linux_distribution/packaging/io.appflowy.appflowy.metainfo.xml $METAINFO +cp -fR $LINUX_RELEASE_PRODUCTION/AppFlowy $LIB +cp ./scripts/linux_distribution/deb/AppFlowy.desktop $APPLICATIONS +cp ./scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml $METAINFO cp ./scripts/linux_distribution/packaging/appflowy.svg $ICONS # Build the package diff --git a/frontend/scripts/linux_distribution/flatpak/README.md b/frontend/scripts/linux_distribution/flatpak/README.md index b1d40056da..7d90cfd2a6 100644 --- a/frontend/scripts/linux_distribution/flatpak/README.md +++ b/frontend/scripts/linux_distribution/flatpak/README.md @@ -1 +1 @@ -Please refer to https://github.com/flathub/io.appflowy.appflowy repo. +Please refer to https://github.com/flathub/io.appflowy.AppFlowy repo. diff --git a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml index ea3b476004..c9a58b68fa 100644 --- a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml +++ b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.metainfo.xml @@ -1,46 +1,38 @@ - io.appflowy.appflowy - + io.appflowy.AppFlowy + AppFlowy - Open Source Notion Alternative - + Open Source Notion Alternative + CC-BY-4.0 AGPL-3.0-only - +

- # Built for teams that need more control and flexibility ## 100% data control You can host - AppFlowy wherever you want; no vendor lock-in. + # Built for teams that need more control and flexibility ## 100% data control You can host AppFlowy wherever you want; no vendor lock-in.

## Unlimited customizations Design and modify AppFlowy your way with an open core codebase.

- ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What - does this mean? Faster development, better native experience, and more reliable performance. + ## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance.

- # Built for individuals who care about data security and mobile experience ## 100% control of - your data Download and install AppFlowy on your local machine. You own and control your - personal data. + # Built for individuals who care about data security and mobile experience ## 100% control of your data Download and install AppFlowy on your local machine. You own and control your personal data.

- ## Extensively extensible For those with no coding experience, AppFlowy enables you to create - apps that suit your needs. It's built on a community-driven toolbox, including templates, - plugins, themes, and more. + ## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It's built on a community-driven toolbox, including templates, plugins, themes, and more.

- ## Truly native experience Faster, more stable with support for offline mode. It's also - better integrated with different devices. Moreover, AppFlowy enables users to access features - and possibilities not available on the web. + ## Truly native experience Faster, more stable with support for offline mode. It's also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web.

- - io.appflowy.appflowy.desktop + + io.appflowy.AppFlowy.desktop https://github.com/AppFlowy-IO/appflowy/raw/main/doc/imgs/welcome.png -
\ No newline at end of file + diff --git a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service index 2982dff894..31e32415d0 100644 --- a/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service +++ b/frontend/scripts/linux_distribution/packaging/io.appflowy.AppFlowy.service @@ -1,3 +1,3 @@ [D-BUS Service] -Name=io.appflowy.appflowy +Name=io.appflowy.AppFlowy Exec=AppFlowy diff --git a/frontend/scripts/linux_distribution/packaging/launcher.sh b/frontend/scripts/linux_distribution/packaging/launcher.sh index 263eca6593..24b4fdbea4 100644 --- a/frontend/scripts/linux_distribution/packaging/launcher.sh +++ b/frontend/scripts/linux_distribution/packaging/launcher.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -gdbus call --session --dest io.appflowy.appflowy \ - --object-path /io/appflowy/appflowy/Object \ - --method io.appflowy.appflowy.Open "['$1']" {} +gdbus call --session --dest io.appflowy.AppFlowy \ + --object-path /io/appflowy/AppFlowy/Object \ + --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst index 82a03114c0..83e1a1043e 100644 --- a/frontend/scripts/linux_installer/postinst +++ b/frontend/scripts/linux_installer/postinst @@ -1,7 +1,7 @@ #!/usr/bin/env bash -if [ -e /usr/local/bin/appflowy ]; then +if [ -e /usr/local/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else echo "Creating Symlink in /usr/local/bin/appflowy" - ln -s /opt/appflowy/appflowy /usr/local/bin/appflowy + ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy fi From f8c18afbcfc39cb3eebfdcf31036126cc9b58211 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 6 Mar 2025 12:39:18 +0800 Subject: [PATCH 073/384] fix: improve the experience of using icon color picker (#7468) --- .../shared/icon_emoji_picker/icon_picker.dart | 189 +++++++++++------- 1 file changed, 118 insertions(+), 71 deletions(-) diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart index 47025225b3..0d57d12d3c 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart @@ -284,83 +284,110 @@ class IconPicker extends StatefulWidget { class _IconPickerState extends State { final mutex = PopoverMutex(); + PopoverController? childPopoverController; + + @override + void dispose() { + super.dispose(); + childPopoverController = null; + } @override Widget build(BuildContext context) { - return ListView.builder( - itemCount: widget.iconGroups.length, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemBuilder: (context, index) { - final iconGroup = widget.iconGroups[index]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - iconGroup.displayName.capitalize(), - fontSize: 12, - figmaLineHeight: 18.0, - color: context.pickerTextColor, - ), - const VSpace(4.0), - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.iconPerLine, - ), - itemCount: iconGroup.icons.length, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, index) { - final icon = iconGroup.icons[index]; - return widget.enableBackgroundColorSelection - ? _Icon( - icon: icon, - mutex: mutex, - onSelectedColor: (context, color) { - String groupName = iconGroup.name; - if (groupName == _kRecentIconGroupName) { - groupName = getGroupName(index); - } - widget.onSelectedIcon( - IconsData( - groupName, - icon.name, - color, - ), + return GestureDetector( + onTap: hideColorSelector, + child: NotificationListener( + onNotification: (notificationInfo) { + if (notificationInfo is ScrollStartNotification) { + hideColorSelector(); + } + return true; + }, + child: ListView.builder( + itemCount: widget.iconGroups.length, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemBuilder: (context, index) { + final iconGroup = widget.iconGroups[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + iconGroup.displayName.capitalize(), + fontSize: 12, + figmaLineHeight: 18.0, + color: context.pickerTextColor, + ), + const VSpace(4.0), + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.iconPerLine, + ), + itemCount: iconGroup.icons.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + final icon = iconGroup.icons[index]; + return widget.enableBackgroundColorSelection + ? _Icon( + icon: icon, + mutex: mutex, + onOpen: (childPopoverController) { + this.childPopoverController = + childPopoverController; + }, + onSelectedColor: (context, color) { + String groupName = iconGroup.name; + if (groupName == _kRecentIconGroupName) { + groupName = getGroupName(index); + } + widget.onSelectedIcon( + IconsData( + groupName, + icon.name, + color, + ), + ); + RecentIcons.putIcon(RecentIcon(icon, groupName)); + PopoverContainer.of(context).close(); + }, + ) + : _IconNoBackground( + icon: icon, + onSelectedIcon: () { + String groupName = iconGroup.name; + if (groupName == _kRecentIconGroupName) { + groupName = getGroupName(index); + } + widget.onSelectedIcon( + IconsData( + groupName, + icon.name, + null, + ), + ); + RecentIcons.putIcon(RecentIcon(icon, groupName)); + }, ); - RecentIcons.putIcon(RecentIcon(icon, groupName)); - PopoverContainer.of(context).close(); - }, - ) - : _IconNoBackground( - icon: icon, - onSelectedIcon: () { - String groupName = iconGroup.name; - if (groupName == _kRecentIconGroupName) { - groupName = getGroupName(index); - } - widget.onSelectedIcon( - IconsData( - groupName, - icon.name, - null, - ), - ); - RecentIcons.putIcon(RecentIcon(icon, groupName)); - }, - ); - }, - ), - const VSpace(12.0), - if (index == widget.iconGroups.length - 1) ...[ - const StreamlinePermit(), - const VSpace(12.0), - ], - ], - ); - }, + }, + ), + const VSpace(12.0), + if (index == widget.iconGroups.length - 1) ...[ + const StreamlinePermit(), + const VSpace(12.0), + ], + ], + ); + }, + ), + ), ); } + void hideColorSelector() { + childPopoverController?.close(); + childPopoverController = null; + } + String getGroupName(int index) { final recentIcons = RecentIcons.getIconsSync(); try { @@ -376,9 +403,11 @@ class _IconNoBackground extends StatelessWidget { const _IconNoBackground({ required this.icon, required this.onSelectedIcon, + this.isSelected = false, }); final Icon icon; + final bool isSelected; final VoidCallback onSelectedIcon; @override @@ -387,6 +416,7 @@ class _IconNoBackground extends StatelessWidget { message: icon.displayName, preferBelow: false, child: FlowyButton( + isSelected: isSelected, useIntrinsicWidth: true, onTap: () => onSelectedIcon(), margin: const EdgeInsets.all(8.0), @@ -408,11 +438,13 @@ class _Icon extends StatefulWidget { required this.icon, required this.mutex, required this.onSelectedColor, + this.onOpen, }); final Icon icon; final PopoverMutex mutex; final void Function(BuildContext context, String color) onSelectedColor; + final ValueChanged? onOpen; @override State<_Icon> createState() => _IconState(); @@ -420,6 +452,7 @@ class _Icon extends StatefulWidget { class _IconState extends State<_Icon> { final PopoverController _popoverController = PopoverController(); + bool isSelected = false; @override void dispose() { @@ -434,10 +467,18 @@ class _IconState extends State<_Icon> { controller: _popoverController, offset: const Offset(0, 6), mutex: widget.mutex, + onClose: () { + updateIsSelected(false); + }, clickHandler: PopoverClickHandler.gestureDetector, child: _IconNoBackground( icon: widget.icon, - onSelectedIcon: () => _popoverController.show(), + isSelected: isSelected, + onSelectedIcon: () { + updateIsSelected(true); + _popoverController.show(); + widget.onOpen?.call(_popoverController); + }, ), popupBuilder: (context) { return Container( @@ -449,6 +490,12 @@ class _IconState extends State<_Icon> { }, ); } + + void updateIsSelected(bool isSelected) { + setState(() { + this.isSelected = isSelected; + }); + } } class StreamlinePermit extends StatelessWidget { From 884586f0afd5c47ef17a15dd8392ed15cb6c8b3e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:09:33 +0800 Subject: [PATCH 074/384] fix: ai writer issues (#7467) * fix: disable ai writer in table * fix: enable header row by default when converting from md * chore: add title when continue writing * chore: rewrite using predefined format * fix: mouse & keyboard event still propagate * chore: bump editor ref --- .../document/presentation/editor_page.dart | 4 +- .../ai/ai_writer_block_component.dart | 23 +++++--- .../ai/ai_writer_toolbar_item.dart | 10 +++- .../ai/operations/ai_writer_cubit.dart | 56 +++++++++++-------- .../parsers/markdown_simple_table_parser.dart | 1 + frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 7 files changed, 63 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index d2716fd716..2f2b2b0aa2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -81,8 +81,8 @@ class _AppFlowyEditorPageState extends State ]; final List toolbarItems = [ - improveWritingItem..isActive = onlyShowInTextType, - aiWriterItem..isActive = onlyShowInTextType, + improveWritingItem..isActive = onlyShowInTextTypeAndExcludeTable, + aiWriterItem..isActive = onlyShowInTextTypeAndExcludeTable, paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, headingsToolbarItem ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index a6a4c80b5d..81a52adb69 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -163,11 +163,8 @@ class _AIWriterBlockComponentState extends State { children: [ BlocBuilder( builder: (context, state) { - final hitTestBehavior = state is GeneratingAiWriterState - ? HitTestBehavior.opaque - : HitTestBehavior.translucent; return GestureDetector( - behavior: hitTestBehavior, + behavior: HitTestBehavior.opaque, onTap: () => onTapOutside(), onTapDown: (_) => onTapOutside(), ); @@ -280,7 +277,7 @@ class OverlayContent extends StatelessWidget { hasSelection: hasSelection, ), onTap: (action) { - context.read().runResponseAction(action); + _onSelectSuggestionAction(context, action); }, ), ), @@ -346,9 +343,7 @@ class OverlayContent extends StatelessWidget { hasSelection: hasSelection, ), onTap: (action) { - context - .read() - .runResponseAction(action); + _onSelectSuggestionAction(context, action); }, ), ], @@ -539,6 +534,18 @@ class OverlayContent extends StatelessWidget { }; } } + + void _onSelectSuggestionAction( + BuildContext context, + SuggestionAction action, + ) { + final predefinedFormat = + context.read().state.predefinedFormat; + context.read().runResponseAction( + action, + predefinedFormat, + ); + } } class MainContentArea extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 714bd5f4b8..f7aa398046 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -8,7 +9,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'ai_writer_block_component.dart'; import 'operations/ai_writer_entities.dart'; const _improveWritingToolbarItemId = 'appflowy.editor.ai_improve_writing'; @@ -223,7 +223,6 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async { command: command, ), ) - ..afterSelection = selection ..selectionExtraInfo = {selectionExtraInfoDisableToolbar: true}; await editorState.apply( @@ -232,6 +231,7 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async { recordUndo: false, inMemoryUpdate: true, ), + withUpdateSelection: false, ); } @@ -240,3 +240,9 @@ bool _isAIEnabled(EditorState editorState) { return documentContext == null || !documentContext.read().isLocalMode; } + +bool onlyShowInTextTypeAndExcludeTable( + EditorState editorState, +) { + return onlyShowInTextType(editorState) && notShowInTable(editorState); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index bf46dd0e9a..56cdacdd6a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; @@ -30,7 +32,7 @@ class AiWriterCubit extends Cubit { isFirstRun: true, ), ) { - editorState.service.keyboardService?.disable(); + editorState.service.keyboardService?.disableShortcuts(); } final String documentId; @@ -47,7 +49,7 @@ class AiWriterCubit extends Cubit { @override Future close() async { selectedSourcesNotifier.dispose(); - editorState.service.keyboardService?.enable(); + editorState.service.keyboardService?.enableShortcuts(); await super.close(); } @@ -184,11 +186,14 @@ class AiWriterCubit extends Cubit { await removeAiWriterNode(editorState, getAiWriterNode()); } - void runResponseAction(SuggestionAction action) async { + void runResponseAction( + SuggestionAction action, [ + PredefinedFormat? predefinedFormat, + ]) async { if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { await _textRobot.discard(); _textRobot.reset(); - runCommand(state.command, null, isRetry: true); + runCommand(state.command, predefinedFormat, isRetry: true); return; } @@ -295,32 +300,39 @@ class AiWriterCubit extends Cubit { end: cursorPosition, ).normalized; - final text = await editorState.getMarkdownInSelection(selection); + String text = await editorState.getMarkdownInSelection(selection); if (text.isEmpty) { if (state is! ReadyAiWriterState) { return; } - final readyState = state as ReadyAiWriterState; - emit( - SingleShotAiWriterState( - command, - title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: - LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(), - onDismiss: () { - if (isImmediateRun) { - removeAiWriterNode(editorState, node); - } - }, - ), - ); - emit(readyState); - return; + final view = await ViewBackendService.getView(documentId).toNullable(); + if (view == null || + view.name.isEmpty || + view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) { + final readyState = state as ReadyAiWriterState; + emit( + SingleShotAiWriterState( + command, + title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: + LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(), + onDismiss: () { + if (isImmediateRun) { + removeAiWriterNode(editorState, node); + } + }, + ), + ); + emit(readyState); + return; + } else { + text += view.name; + } } final stream = await _aiService.streamCompletion( objectId: documentId, - text: await editorState.getMarkdownInSelection(selection), + text: text, completionType: command.toCompletionType(), onStart: () async { final transaction = editorState.transaction; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart index a857d1ca8f..09973021f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart @@ -106,6 +106,7 @@ class MarkdownSimpleTableParser extends CustomMarkdownParser { return [ simpleTableBlockNode( children: rows, + enableHeaderRow: true, columnWidths: UniversalPlatform.isMobile || tableWidth == null ? null : {for (var i = 0; i < th.length; i++) i.toString(): tableWidth!}, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 043b11cde5..c54759cb71 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: b2672f2 - resolved-ref: b2672f297b0e9e7a028ea217ae6a57c8dca3da4e + ref: "866aea4" + resolved-ref: "866aea4daca836d5734df4fca20563338755e82a" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 6dc31cf237..4375cd0ae2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "b2672f2" + ref: "866aea4" appflowy_editor_plugins: git: From fc0fb0b3d357780045d28b4a3b4f17390d5f8b8d Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 6 Mar 2025 16:02:56 +0800 Subject: [PATCH 075/384] fix: update locked page button background color (#7470) --- .../presentation/widgets/view_title_bar.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 87452d560e..04ae9b30ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -400,7 +400,7 @@ class LockedPageStatus extends StatelessWidget { side: BorderSide(color: color), borderRadius: BorderRadius.circular(6), ), - color: Colors.white.withValues(alpha: 0.75), + color: context.lockedPageButtonBackground, ), child: FlowyButton( useIntrinsicWidth: true, @@ -440,7 +440,7 @@ class ReLockedPageStatus extends StatelessWidget { side: BorderSide(color: iconColor), borderRadius: BorderRadius.circular(6), ), - color: Colors.white.withValues(alpha: 0.75), + color: context.lockedPageButtonBackground, ), child: FlowyButton( useIntrinsicWidth: true, @@ -465,3 +465,12 @@ class ReLockedPageStatus extends StatelessWidget { ); } } + +extension on BuildContext { + Color get lockedPageButtonBackground { + if (Theme.of(this).brightness == Brightness.light) { + return Colors.white.withValues(alpha: 0.75); + } + return Color(0xB21B1A22); + } +} From 3d3f81ad524cb68c231572fd99e5799ac18b9a92 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 6 Mar 2025 16:04:54 +0800 Subject: [PATCH 076/384] fix: complete the missing icons in the database (#7464) * fix: complete the missing icons in the database * fix: the toggle is slower than the actual change taken into effect --- .../board/presentation/toolbar/board_setting_bar.dart | 9 +++++++++ .../presentation/toolbar/calendar_setting_bar.dart | 9 +++++++++ .../presentation/widgets/toolbar/grid_setting_bar.dart | 10 ++++++---- .../widgets/setting/database_layout_selector.dart | 1 + .../workspace/presentation/widgets/toggle/toggle.dart | 7 ++++--- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart index 9e3203e093..e57364b2d8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart @@ -2,10 +2,13 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; class BoardSettingBar extends StatelessWidget { const BoardSettingBar({ @@ -30,6 +33,8 @@ class BoardSettingBar extends StatelessWidget { if (value) { return const SizedBox.shrink(); } + final isReference = + Provider.of(context)?.isReference ?? false; return SizedBox( height: 20, child: Row( @@ -38,6 +43,10 @@ class BoardSettingBar extends StatelessWidget { FilterButton( toggleExtension: toggleExtension, ), + if (isReference) ...[ + const HSpace(2), + ViewDatabaseButton(view: databaseController.view), + ], const HSpace(2), SettingButton( databaseController: databaseController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart index c2307c63a5..6bfe7b99a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart @@ -2,10 +2,13 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; class CalendarSettingBar extends StatelessWidget { const CalendarSettingBar({ @@ -30,6 +33,8 @@ class CalendarSettingBar extends StatelessWidget { if (value) { return const SizedBox.shrink(); } + final isReference = + Provider.of(context)?.isReference ?? false; return SizedBox( height: 20, child: Row( @@ -38,6 +43,10 @@ class CalendarSettingBar extends StatelessWidget { FilterButton( toggleExtension: toggleExtension, ), + if (isReference) ...[ + const HSpace(2), + ViewDatabaseButton(view: databaseController.view), + ], const HSpace(2), SettingButton( databaseController: databaseController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart index c44341e214..f325ab206f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -54,12 +54,14 @@ class GridSettingBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ FilterButton(toggleExtension: toggleExtension), - const HSpace(6), + const HSpace(2), SortButton(toggleExtension: toggleExtension), - const HSpace(6), + if (isReference) ...[ + const HSpace(2), + ViewDatabaseButton(view: controller.view), + ], + const HSpace(2), SettingButton(databaseController: controller), - if (isReference) const HSpace(6), - if (isReference) ViewDatabaseButton(view: controller.view), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart index dd600241db..b4ee4134c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart @@ -81,6 +81,7 @@ class DatabaseLayoutSelector extends StatelessWidget { builder: (context, compactMode, child) { return Toggle( value: compactMode, + duration: Duration.zero, onChanged: (value) => databaseController.setCompactMode(value), padding: EdgeInsets.zero, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart index 673f08c668..5cb834cbf3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; class ToggleStyle { const ToggleStyle({ @@ -38,6 +37,7 @@ class Toggle extends StatelessWidget { this.thumbColor, this.activeBackgroundColor, this.inactiveBackgroundColor, + this.duration = const Duration(milliseconds: 150), this.padding = const EdgeInsets.all(8.0), }); @@ -48,6 +48,7 @@ class Toggle extends StatelessWidget { final Color? activeBackgroundColor; final Color? inactiveBackgroundColor; final EdgeInsets padding; + final Duration duration; @override Widget build(BuildContext context) { @@ -70,7 +71,7 @@ class Toggle extends StatelessWidget { ), ), AnimatedPositioned( - duration: const Duration(milliseconds: 150), + duration: duration, top: (style.height - style.thumbRadius) / 2, left: value ? style.width - style.thumbRadius - 1 : 1, child: Container( From a062c4aadbfb2677d2854e256a483f075d0b419c Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 6 Mar 2025 16:59:58 +0800 Subject: [PATCH 077/384] fix: bulleted list icon does not center in the columns (#7471) --- .../editor_plugins/bulleted_list/bulleted_list_icon.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart index 43b84ddc1c..23b73e75a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart @@ -42,10 +42,8 @@ class BulletedListIcon extends StatelessWidget { size: Size.square(size * 0.8), ); return Container( - constraints: BoxConstraints( - minWidth: size, - minHeight: size, - ), + width: size, + height: size, margin: const EdgeInsets.only(right: 8.0), alignment: Alignment.center, child: icon, From 7f3469a0f2bfed87bf94b420be7845711dfbcf45 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 6 Mar 2025 18:22:10 +0800 Subject: [PATCH 078/384] feat: use undo to revert the autotypograph (#7472) --- .../base/format_arrow_character.dart | 43 ++++++++++++++++++- .../shortcuts/character_shortcuts.dart | 2 + .../shortcuts/format_shortcut_test.dart | 41 ++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart index 379c7c61ba..8548b9354c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart @@ -7,6 +7,9 @@ const _equals = '='; const _equalGreater = '⇒'; const _dashGreater = '→'; +const _hyphen = '-'; +const _emDash = '—'; // This is an em dash — not a single dash - !! + /// format '=' + '>' into an ⇒ /// /// - support @@ -43,6 +46,24 @@ final CharacterShortcutEvent customFormatDashGreater = CharacterShortcutEvent( ), ); +/// format two hyphens into an em dash +/// +/// - support +/// - desktop +/// - mobile +/// - web +/// +final CharacterShortcutEvent customFormatDoubleHyphenEmDash = + CharacterShortcutEvent( + key: 'format double hyphen into an em dash', + character: _hyphen, + handler: (editorState) async => _handleDoubleCharacterReplacement( + editorState: editorState, + character: _hyphen, + replacement: _emDash, + ), +); + /// If [prefixCharacter] is null or empty, [character] is used Future _handleDoubleCharacterReplacement({ required EditorState editorState, @@ -81,11 +102,29 @@ Future _handleDoubleCharacterReplacement({ return false; } + // insert the greater character first and convert it to the replacement character to support undo + final insert = editorState.transaction + ..insertText( + node, + selection.end.offset, + character, + ); + + await editorState.apply( + insert, + skipHistoryDebounce: true, + ); + + final afterSelection = editorState.selection; + if (afterSelection == null) { + return false; + } + final replace = editorState.transaction ..replaceText( node, - selection.end.offset - 1, - 1, + afterSelection.end.offset - 2, + 2, replacement, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart index 158d14f48e..49178ca12b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart @@ -44,6 +44,7 @@ List buildCharacterShortcutEvents( customFormatGreaterEqual, customFormatDashGreater, + customFormatDoubleHyphenEmDash, customFormatNumberToNumberedList, customFormatSignToHeading, @@ -55,6 +56,7 @@ List buildCharacterShortcutEvents( formatGreaterEqual, // Overridden by customFormatGreaterEqual formatNumberToNumberedList, // Overridden by customFormatNumberToNumberedList formatSignToHeading, // Overridden by customFormatSignToHeading + formatDoubleHyphenEmDash, // Overridden by customFormatDoubleHyphenEmDash ].contains(shortcut), ), diff --git a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart b/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart index c5cb1c524d..21011df540 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart @@ -33,6 +33,12 @@ void main() { final node = editorState.document.root.children[0]; expect(node.delta!.toPlainText(), '⇒'); + // use undo to revert the change + undoCommand.execute(editorState); + expect(editorState.document.root.children.length, 1); + final nodeAfterUndo = editorState.document.root.children[0]; + expect(nodeAfterUndo.delta!.toPlainText(), '=>'); + editorState.dispose(); }); @@ -56,6 +62,41 @@ void main() { final node = editorState.document.root.children[0]; expect(node.delta!.toPlainText(), '→'); + // use undo to revert the change + undoCommand.execute(editorState); + expect(editorState.document.root.children.length, 1); + final nodeAfterUndo = editorState.document.root.children[0]; + expect(nodeAfterUndo.delta!.toPlainText(), '->'); + + editorState.dispose(); + }); + + test('turn -- into —', () async { + final document = Document.blank() + ..insert([ + 0, + ], [ + paragraphNode(text: '-'), + ]); + + final editorState = EditorState(document: document); + editorState.selection = Selection.collapsed( + Position(path: [0], offset: 1), + ); + + final result = await customFormatDoubleHyphenEmDash.execute(editorState); + expect(result, true); + + expect(editorState.document.root.children.length, 1); + final node = editorState.document.root.children[0]; + expect(node.delta!.toPlainText(), '—'); + + // use undo to revert the change + undoCommand.execute(editorState); + expect(editorState.document.root.children.length, 1); + final nodeAfterUndo = editorState.document.root.children[0]; + expect(nodeAfterUndo.delta!.toPlainText(), '--'); + editorState.dispose(); }); }); From 556d929b677735f04f70deb186968d7b8ba5260b Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 6 Mar 2025 18:45:27 +0800 Subject: [PATCH 079/384] fix: error caused by ScrollablePositionedList(#7460) (#7469) * fix: error caused by ScrollablePositionedList * chore: update appflowy_editor version --- .../editor_plugins/base/selectable_item_list_menu.dart | 6 +++--- .../code_block/code_block_language_selector.dart | 3 ++- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart index 0b6382fc53..3c997bbdc4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart @@ -1,9 +1,9 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - +// ignore: implementation_imports +import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:flutter/material.dart'; class SelectableItemListMenu extends StatelessWidget { const SelectableItemListMenu({ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart index 14b19a6927..c4c2e3e0ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart @@ -3,6 +3,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -10,7 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:universal_platform/universal_platform.dart'; CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c54759cb71..09e6287908 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "866aea4" - resolved-ref: "866aea4daca836d5734df4fca20563338755e82a" + ref: e2c9713 + resolved-ref: e2c9713104f00658c83ac7d1657e7a0fade171ad url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 4375cd0ae2..c1429213c8 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "866aea4" + ref: "e2c9713" appflowy_editor_plugins: git: From 68e7069e926dd57844f1ebd15a5c9a50f4de0d31 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 6 Mar 2025 22:03:30 +0800 Subject: [PATCH 080/384] chore: bump version 0.8.6 & update changelog (#7473) * chore: bump version 0.8.6 * chore: update changelog --- CHANGELOG.md | 13 +++++++++++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c47e17aa1..be3a1e8b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,17 @@ # Release Notes +## Version 0.8.6 - 06/03/2025 +### Bug Fixes +- Fix the incorrect title positioning when adjusting the document width setting +- Enhance the user experience of the icon color picker for smoother interactions +- Add missing icons to the database to ensure completeness and consistency +- Resolve the issue with links not functioning correctly on Linux systems +- Improve the outline feature to work seamlessly within columns +- Center the bulleted list icon within columns for better visual alignment +- Enable dragging blocks under tables in the second column to enhance flexibility +- Disable the AI writer feature within tables to prevent conflicts and improve usability +- Automatically enable the header row when converting content from Markdown to ensure proper formatting +- Use the "Undo" function to revert the auto-formatting + ## Version 0.8.5 - 04/03/2025 ### New Features - Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 6195e6a228..89ee36ad6a 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.5" +APPFLOWY_VERSION = "0.8.6" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index c1429213c8..f70f88ede7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.5 +version: 0.8.6 environment: flutter: ">=3.27.4" From ea18aa7551ab2eb380f401a8ff3b828c13d0c747 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 7 Mar 2025 11:17:56 +0800 Subject: [PATCH 081/384] chore: bump collab version 45239d2 (#7477) --- frontend/rust-lib/Cargo.lock | 16 ++++++++-------- frontend/rust-lib/Cargo.toml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 50cfd1cc7e..c447cc03bb 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -896,7 +896,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -921,7 +921,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-trait", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "bytes", @@ -1002,7 +1002,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -1024,7 +1024,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-recursion", @@ -1088,7 +1088,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-stream", @@ -1165,7 +1165,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 27c81a9f96..7a74744060 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } # Working directory: frontend # To update the commit ID, run: From a0ae62d6f502e261b4f292373626df38a18c0277 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:32:27 +0800 Subject: [PATCH 082/384] fix: consider simple table in exclude table types (#7478) --- .../editor_plugins/base/toolbar_extension.dart | 9 +++++++-- frontend/appflowy_flutter/pubspec.lock | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart index ebbfb27db6..aed78172b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart @@ -1,5 +1,10 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +bool _isTableType(String type) { + return [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(type); +} + bool notShowInTable(EditorState editorState) { final selection = editorState.selection; if (selection == null) { @@ -7,12 +12,12 @@ bool notShowInTable(EditorState editorState) { } final nodes = editorState.getNodesInSelection(selection); return nodes.every((element) { - if (element.type == TableBlockKeys.type) { + if (_isTableType(element.type)) { return false; } var parent = element.parent; while (parent != null) { - if (parent.type == TableBlockKeys.type) { + if (_isTableType(parent.type)) { return false; } parent = parent.parent; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 09e6287908..fc04deae5b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1583,10 +1583,10 @@ packages: dependency: transitive description: name: pdf - sha256: adbdec5bc84d20e6c8d67f9c64270aa64d1e9e1ed529f0fef7e7bc7e9400f894 + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" url: "https://pub.dev" source: hosted - version: "3.11.2" + version: "3.11.3" percent_indicator: dependency: "direct main" description: From 4ff71b5dcef2089f4b15c3b4bbf0aa2df0fe5564 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 9 Mar 2025 23:32:42 +0800 Subject: [PATCH 083/384] chore: implement ollama --- .../lib/ai/service/ai_prompt_input_bloc.dart | 54 +- .../desktop_prompt_text_field.dart | 2 +- .../settings/ai/download_model_bloc.dart | 167 ------ .../ai/download_offline_ai_app_bloc.dart | 2 +- .../settings/ai/local_ai_chat_bloc.dart | 251 +------- .../ai/local_ai_chat_toggle_bloc.dart | 23 +- .../settings/ai/local_llm_listener.dart | 19 +- .../settings/ai/ollama_setting_bloc.dart | 221 ++++++++ .../settings/ai/plugin_state_bloc.dart | 86 +-- .../setting_ai_view/downloading_model.dart | 119 ---- .../pages/setting_ai_view/init_local_ai.dart | 4 +- .../local_ai_chat_setting.dart | 370 ------------ .../setting_ai_view/local_ai_setting.dart | 4 +- .../local_ai_setting_panel.dart | 125 ++++ .../pages/setting_ai_view/ollma_setting.dart | 165 ++++++ .../pages/setting_ai_view/plugin_state.dart | 82 +-- frontend/resources/translations/en.json | 7 +- frontend/rust-lib/Cargo.lock | 59 +- frontend/rust-lib/Cargo.toml | 6 +- frontend/rust-lib/collab-integrate/Cargo.toml | 2 +- frontend/rust-lib/dart-ffi/Cargo.toml | 2 +- .../event-integration-test/Cargo.toml | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 33 +- frontend/rust-lib/flowy-ai/src/entities.rs | 134 ++--- .../rust-lib/flowy-ai/src/event_handler.rs | 203 ++----- frontend/rust-lib/flowy-ai/src/event_map.rs | 78 +-- .../{local_llm_chat.rs => controller.rs} | 435 +++++++------- .../src/local_ai/local_llm_resource.rs | 534 ------------------ .../rust-lib/flowy-ai/src/local_ai/mod.rs | 6 +- .../local_ai/{model_request.rs => request.rs} | 2 + .../flowy-ai/src/local_ai/resource.rs | 315 +++++++++++ .../rust-lib/flowy-ai/src/local_ai/watch.rs | 8 +- .../src/middleware/chat_service_mw.rs | 40 +- .../rust-lib/flowy-ai/src/notification.rs | 11 +- .../src/deps_resolve/database_deps.rs | 8 +- frontend/rust-lib/flowy-folder/Cargo.toml | 2 +- frontend/rust-lib/lib-infra/Cargo.toml | 4 +- 37 files changed, 1370 insertions(+), 2215 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart rename frontend/rust-lib/flowy-ai/src/local_ai/{local_llm_chat.rs => controller.rs} (50%) delete mode 100644 frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs rename frontend/rust-lib/flowy-ai/src/local_ai/{model_request.rs => request.rs} (99%) create mode 100644 frontend/rust-lib/flowy-ai/src/local_ai/resource.rs diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 23d8879275..2dff6ccbd2 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -36,36 +36,19 @@ class AIPromptInputBloc extends Bloc { on( (event, emit) { event.when( - updateChatState: (LocalAIChatPB chatState) { + updateAIState: (LocalAIPB localAIState) { // Only user enable chat with file and the plugin is already running - final supportChatWithFile = chatState.fileEnabled && - chatState.pluginState.state == RunningStatePB.Running; + final supportChatWithFile = localAIState.enabled && + localAIState.state == RunningStatePB.Running; - final aiType = chatState.pluginState.state == RunningStatePB.Running - ? AIType.localAI - : AIType.appflowyAI; + final aiType = + localAIState.enabled ? AIType.localAI : AIType.appflowyAI; emit( state.copyWith( aiType: aiType, supportChatWithFile: supportChatWithFile, - chatState: chatState, - ), - ); - }, - updatePluginState: (LocalAIPluginStatePB chatState) { - final fileEnabled = state.chatState?.fileEnabled ?? false; - final supportChatWithFile = - fileEnabled && chatState.state == RunningStatePB.Running; - - final aiType = chatState.state == RunningStatePB.Running - ? AIType.localAI - : AIType.appflowyAI; - - emit( - state.copyWith( - supportChatWithFile: supportChatWithFile, - aiType: aiType, + localAIState: localAIState, ), ); }, @@ -130,22 +113,17 @@ class AIPromptInputBloc extends Bloc { _listener.start( stateCallback: (pluginState) { if (!isClosed) { - add(AIPromptInputEvent.updatePluginState(pluginState)); - } - }, - chatStateCallback: (chatState) { - if (!isClosed) { - add(AIPromptInputEvent.updateChatState(chatState)); + add(AIPromptInputEvent.updateAIState(pluginState)); } }, ); } void _init() { - AIEventGetLocalAIChatState().send().fold( - (chatState) { + AIEventGetLocalAIState().send().fold( + (localAIState) { if (!isClosed) { - add(AIPromptInputEvent.updateChatState(chatState)); + add(AIPromptInputEvent.updateAIState(localAIState)); } }, Log.error, @@ -168,12 +146,8 @@ class AIPromptInputBloc extends Bloc { @freezed class AIPromptInputEvent with _$AIPromptInputEvent { - const factory AIPromptInputEvent.updateChatState( - LocalAIChatPB chatState, - ) = _UpdateChatState; - const factory AIPromptInputEvent.updatePluginState( - LocalAIPluginStatePB chatState, - ) = _UpdatePluginState; + const factory AIPromptInputEvent.updateAIState(LocalAIPB localAIState) = + _UpdateAIState; const factory AIPromptInputEvent.toggleShowPredefinedFormat() = _ToggleShowPredefinedFormat; const factory AIPromptInputEvent.updatePredefinedFormat( @@ -196,7 +170,7 @@ class AIPromptInputState with _$AIPromptInputState { required bool supportChatWithFile, required bool showPredefinedFormats, required PredefinedFormat? predefinedFormat, - required LocalAIChatPB? chatState, + required LocalAIPB? localAIState, required List attachedFiles, required List mentionedPages, }) = _AIPromptInputState; @@ -207,7 +181,7 @@ class AIPromptInputState with _$AIPromptInputState { supportChatWithFile: false, showPredefinedFormats: format != null, predefinedFormat: format, - chatState: null, + localAIState: null, attachedFiles: [], mentionedPages: [], ); diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index e2eeb80434..0905e9b6bc 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -572,7 +572,7 @@ class _PromptBottomActions extends StatelessWidget { margin: DesktopAIChatSizes.inputActionBarMargin, child: BlocBuilder( builder: (context, state) { - if (state.chatState == null) { + if (state.localAIState == null) { return Align( alignment: AlignmentDirectional.centerEnd, child: _sendButton(), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart deleted file mode 100644 index 8be68e813e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:fixnum/fixnum.dart'; -part 'download_model_bloc.freezed.dart'; - -class DownloadModelBloc extends Bloc { - DownloadModelBloc(LLMModelPB model) - : super(DownloadModelState.initial(model)) { - on(_handleEvent); - } - - Future _handleEvent( - DownloadModelEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final downloadStream = DownloadingStream(); - downloadStream.listen( - onModelPercentage: (name, percent) { - if (!isClosed) { - add( - DownloadModelEvent.updatePercent(name, percent), - ); - } - }, - onPluginPercentage: (percent) { - if (!isClosed) { - add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent)); - } - }, - onFinish: () { - add(const DownloadModelEvent.downloadFinish()); - }, - onError: (err) { - Log.error(err); - }, - ); - - final payload = - DownloadLLMPB(progressStream: Int64(downloadStream.nativePort)); - final result = await AIEventDownloadLLMResource(payload).send(); - result.fold((_) { - emit( - state.copyWith( - downloadStream: downloadStream, - loadingState: const ChatLoadingState.finish(), - downloadError: null, - ), - ); - }, (err) { - emit( - state.copyWith( - loadingState: ChatLoadingState.finish(error: err), - ), - ); - }); - }, - updatePercent: (String object, double percent) { - emit(state.copyWith(object: object, percent: percent)); - }, - downloadFinish: () { - emit(state.copyWith(isFinish: true)); - }, - ); - } - - @override - Future close() async { - await state.downloadStream?.dispose(); - return super.close(); - } -} - -@freezed -class DownloadModelEvent with _$DownloadModelEvent { - const factory DownloadModelEvent.started() = _Started; - const factory DownloadModelEvent.updatePercent( - String object, - double percent, - ) = _UpdatePercent; - const factory DownloadModelEvent.downloadFinish() = _DownloadFinish; -} - -@freezed -class DownloadModelState with _$DownloadModelState { - const factory DownloadModelState({ - required LLMModelPB model, - DownloadingStream? downloadStream, - String? downloadError, - @Default("") String object, - @Default(0) double percent, - @Default(false) bool isFinish, - String? bigFileDownloadPrompt, - @Default(ChatLoadingState.loading()) ChatLoadingState loadingState, - }) = _DownloadModelState; - - factory DownloadModelState.initial(LLMModelPB model) { - // bigger than 1 GB then show download big file prompt - String? bigFileDownloadPrompt; - if (model.fileSize > 1 * 1024 * 1024 * 1024) { - bigFileDownloadPrompt = - LocaleKeys.settings_aiPage_keys_downloadBigFilePrompt.tr(); - } - return DownloadModelState( - model: model, - bigFileDownloadPrompt: bigFileDownloadPrompt, - ); - } -} - -class DownloadingStream { - DownloadingStream() { - _port.handler = _controller.add; - } - - final RawReceivePort _port = RawReceivePort(); - StreamSubscription? _sub; - final StreamController _controller = StreamController.broadcast(); - int get nativePort => _port.sendPort.nativePort; - - Future dispose() async { - await _sub?.cancel(); - await _controller.close(); - _port.close(); - } - - void listen({ - void Function(String modelName, double percent)? onModelPercentage, - void Function(double percent)? onPluginPercentage, - void Function(String data)? onError, - void Function()? onFinish, - }) { - _sub = _controller.stream.listen((text) { - if (text.contains(':progress:')) { - final progressIndex = text.indexOf(':progress:'); - final modelName = text.substring(0, progressIndex); - final progressValue = text - .substring(progressIndex + 10); // 10 is the length of ":progress:" - final percent = double.tryParse(progressValue); - if (percent != null) { - onModelPercentage?.call(modelName, percent); - } - } else if (text.startsWith('plugin:progress:')) { - final percent = double.tryParse(text.substring(16)); - if (percent != null) { - onPluginPercentage?.call(percent); - } - } else if (text.startsWith('finish')) { - onFinish?.call(); - } else if (text.startsWith('error:')) { - // substring 6 to remove "error:" - onError?.call(text.substring(6)); - } - }); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart index 829bd2f62a..185a8c049f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart @@ -18,7 +18,7 @@ class DownloadOfflineAIBloc ) async { await event.when( started: () async { - final result = await AIEventGetOfflineAIAppLink().send(); + final result = await AIEventGetLocalAIDownloadLink().send(); await result.fold( (app) async { await launchUrl(Uri.parse(app.link)); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart index 7f1df258ea..c254246213 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart @@ -1,185 +1,61 @@ import 'dart:async'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'local_ai_chat_bloc.freezed.dart'; -class LocalAIChatSettingBloc - extends Bloc { - LocalAIChatSettingBloc() +class LocalAISettingPanelBloc + extends Bloc { + LocalAISettingPanelBloc() : listener = LocalLLMListener(), - super(const LocalAIChatSettingState()) { + super(const LocalAISettingPanelState()) { listener.start( stateCallback: (newState) { if (!isClosed) { - add(LocalAIChatSettingEvent.updatePluginState(newState)); + add(LocalAISettingPanelEvent.updateAIState(newState)); } }, ); - on(_handleEvent); + on(_handleEvent); } final LocalLLMListener listener; /// Handles incoming events and dispatches them to the appropriate handler. Future _handleEvent( - LocalAIChatSettingEvent event, - Emitter emit, + LocalAISettingPanelEvent event, + Emitter emit, ) async { - await event.when( - refreshAISetting: _handleStarted, - didLoadModelInfo: (FlowyResult result) { - result.fold( - (modelInfo) { - _fetchCurremtLLMState(); - emit( - state.copyWith( - modelInfo: modelInfo, - models: modelInfo.models, - selectedLLMModel: modelInfo.selectedModel, - aiModelProgress: const AIModelProgress.finish(), - ), - ); - }, - (err) { - emit( - state.copyWith( - aiModelProgress: AIModelProgress.finish(error: err), - ), - ); - }, - ); - }, - selectLLMConfig: (LLMModelPB llmModel) async { - final result = await AIEventUpdateLocalLLM(llmModel).send(); - result.fold( - (llmResource) { - // If all resources are downloaded, show reload plugin - if (llmResource.pendingResources.isNotEmpty) { - emit( - state.copyWith( - selectedLLMModel: llmModel, - progressIndicator: LocalAIProgress.showDownload( - llmResource, - llmModel, - ), - selectLLMState: const ChatLoadingState.finish(), - ), - ); - } else { - emit( - state.copyWith( - selectedLLMModel: llmModel, - selectLLMState: const ChatLoadingState.finish(), - progressIndicator: const LocalAIProgress.checkPluginState(), - ), - ); + event.when( + started: () { + AIEventGetLocalAIState().send().fold( + (localAIState) { + if (!isClosed) { + add(LocalAISettingPanelEvent.updateAIState(localAIState)); } }, - (err) { - emit( - state.copyWith( - selectLLMState: ChatLoadingState.finish(error: err), - ), - ); - }, + Log.error, ); }, - refreshLLMState: (LocalModelResourcePB llmResource) { - if (state.selectedLLMModel == null) { - Log.error( - 'Unexpected null selected config. It should be set already', - ); - return; - } - - // reload plugin if all resources are downloaded - if (llmResource.pendingResources.isEmpty) { + updateAIState: (LocalAIPB pluginState) { + if (pluginState.isAppDownloaded) { emit( state.copyWith( + runningState: pluginState.state, progressIndicator: const LocalAIProgress.checkPluginState(), ), ); - } else { - if (state.selectedLLMModel != null) { - // Go to download page if the selected model is downloading - if (llmResource.isDownloading) { - emit( - state.copyWith( - progressIndicator: - LocalAIProgress.startDownloading(state.selectedLLMModel!), - selectLLMState: const ChatLoadingState.finish(), - ), - ); - return; - } else { - emit( - state.copyWith( - progressIndicator: LocalAIProgress.showDownload( - llmResource, - state.selectedLLMModel!, - ), - selectLLMState: const ChatLoadingState.finish(), - ), - ); - } - } - } - }, - startDownloadModel: (LLMModelPB llmModel) { - emit( - state.copyWith( - progressIndicator: LocalAIProgress.startDownloading(llmModel), - selectLLMState: const ChatLoadingState.finish(), - ), - ); - }, - cancelDownload: () async { - final _ = await AIEventCancelDownloadLLMResource().send(); - _fetchCurremtLLMState(); - }, - finishDownload: () async { - emit( - state.copyWith( - progressIndicator: const LocalAIProgress.finishDownload(), - ), - ); - }, - updatePluginState: (LocalAIPluginStatePB pluginState) { - if (pluginState.offlineAiReady) { - AIEventRefreshLocalAIModelInfo().send().then((result) { - if (!isClosed) { - add(LocalAIChatSettingEvent.didLoadModelInfo(result)); - } - }); - - if (pluginState.state == RunningStatePB.Stopped) { - emit( - state.copyWith( - runningState: pluginState.state, - progressIndicator: const LocalAIProgress.checkPluginState(), - ), - ); - } else { - emit( - state.copyWith( - runningState: pluginState.state, - ), - ); - } } else { emit( state.copyWith( - progressIndicator: const LocalAIProgress.startOfflineAIApp(), + progressIndicator: const LocalAIProgress.downloadLocalAIApp(), ), ); } @@ -187,39 +63,6 @@ class LocalAIChatSettingBloc ); } - void _fetchCurremtLLMState() async { - final result = await AIEventGetLocalLLMState().send(); - result.fold( - (llmResource) { - if (!isClosed) { - add(LocalAIChatSettingEvent.refreshLLMState(llmResource)); - } - }, - (err) { - Log.error(err); - }, - ); - } - - /// Handles the event to fetch local AI settings when the application starts. - Future _handleStarted() async { - final result = await AIEventGetLocalAIPluginState().send(); - result.fold( - (pluginState) async { - if (!isClosed) { - add(LocalAIChatSettingEvent.updatePluginState(pluginState)); - if (pluginState.offlineAiReady) { - final result = await AIEventRefreshLocalAIModelInfo().send(); - if (!isClosed) { - add(LocalAIChatSettingEvent.didLoadModelInfo(result)); - } - } - } - }, - (err) => Log.error(err.toString()), - ); - } - @override Future close() async { await listener.stop(); @@ -228,60 +71,24 @@ class LocalAIChatSettingBloc } @freezed -class LocalAIChatSettingEvent with _$LocalAIChatSettingEvent { - const factory LocalAIChatSettingEvent.refreshAISetting() = _RefreshAISetting; - const factory LocalAIChatSettingEvent.didLoadModelInfo( - FlowyResult result, - ) = _ModelInfo; - const factory LocalAIChatSettingEvent.selectLLMConfig(LLMModelPB config) = - _SelectLLMConfig; - - const factory LocalAIChatSettingEvent.refreshLLMState( - LocalModelResourcePB llmResource, - ) = _RefreshLLMResource; - const factory LocalAIChatSettingEvent.startDownloadModel( - LLMModelPB llmModel, - ) = _StartDownloadModel; - - const factory LocalAIChatSettingEvent.cancelDownload() = _CancelDownload; - const factory LocalAIChatSettingEvent.finishDownload() = _FinishDownload; - const factory LocalAIChatSettingEvent.updatePluginState( - LocalAIPluginStatePB pluginState, - ) = _PluginState; +class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent { + const factory LocalAISettingPanelEvent.started() = _Started; + const factory LocalAISettingPanelEvent.updateAIState( + LocalAIPB aiState, + ) = _UpdateAIState; } @freezed -class LocalAIChatSettingState with _$LocalAIChatSettingState { - const factory LocalAIChatSettingState({ - LLMModelInfoPB? modelInfo, - LLMModelPB? selectedLLMModel, +class LocalAISettingPanelState with _$LocalAISettingPanelState { + const factory LocalAISettingPanelState({ LocalAIProgress? progressIndicator, - @Default(AIModelProgress.init()) AIModelProgress aiModelProgress, - @Default(ChatLoadingState.loading()) ChatLoadingState selectLLMState, - @Default([]) List models, @Default(RunningStatePB.Connecting) RunningStatePB runningState, }) = _LocalAIChatSettingState; } @freezed class LocalAIProgress with _$LocalAIProgress { - // when user comes back to the setting page, it will auto detect current llm state - const factory LocalAIProgress.showDownload( - LocalModelResourcePB llmResource, - LLMModelPB llmModel, - ) = _DownloadNeeded; - - // when start downloading the model - const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) = - _Downloading; - const factory LocalAIProgress.finishDownload() = _Finish; - const factory LocalAIProgress.checkPluginState() = _CheckPluginState; - const factory LocalAIProgress.startOfflineAIApp() = _StartOfflineAIApp; -} - -@freezed -class AIModelProgress with _$AIModelProgress { - const factory AIModelProgress.init() = _AIModelProgressInit; - const factory AIModelProgress.loading() = _AIModelDownloading; - const factory AIModelProgress.finish({FlowyError? error}) = _AIModelFinish; + const factory LocalAIProgress.checkPluginState() = _CheckPluginStateProgress; + const factory LocalAIProgress.downloadLocalAIApp() = + _DownloadLocalAIAppProgress; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart index 4feac1247a..f533778f9b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart @@ -20,25 +20,9 @@ class LocalAIChatToggleBloc ) async { await event.when( started: () async { - final result = await AIEventGetLocalAIChatState().send(); + final result = await AIEventGetLocalAIState().send(); _handleResult(emit, result); }, - toggle: () async { - emit( - state.copyWith( - pageIndicator: const LocalAIChatToggleStateIndicator.loading(), - ), - ); - unawaited( - AIEventToggleLocalAIChat().send().then( - (result) { - if (!isClosed) { - add(LocalAIChatToggleEvent.handleResult(result)); - } - }, - ), - ); - }, handleResult: (result) { _handleResult(emit, result); }, @@ -47,7 +31,7 @@ class LocalAIChatToggleBloc void _handleResult( Emitter emit, - FlowyResult result, + FlowyResult result, ) { result.fold( (localAI) { @@ -72,9 +56,8 @@ class LocalAIChatToggleBloc @freezed class LocalAIChatToggleEvent with _$LocalAIChatToggleEvent { const factory LocalAIChatToggleEvent.started() = _Started; - const factory LocalAIChatToggleEvent.toggle() = _Toggle; const factory LocalAIChatToggleEvent.handleResult( - FlowyResult result, + FlowyResult result, ) = _HandleResult; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart index a7778d7d99..40cdb564b2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -8,8 +8,8 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; -typedef PluginStateCallback = void Function(LocalAIPluginStatePB state); -typedef LocalAIChatCallback = void Function(LocalAIChatPB chatState); +typedef PluginStateCallback = void Function(LocalAIPB state); +typedef PluginResourceCallback = void Function(LackOfAIResourcePB data); class LocalLLMListener { LocalLLMListener() { @@ -24,15 +24,14 @@ class LocalLLMListener { ChatNotificationParser? _parser; PluginStateCallback? stateCallback; - LocalAIChatCallback? chatStateCallback; - void Function()? finishStreamingCallback; + PluginResourceCallback? resourceCallback; void start({ PluginStateCallback? stateCallback, - LocalAIChatCallback? chatStateCallback, + PluginResourceCallback? resourceCallback, }) { this.stateCallback = stateCallback; - this.chatStateCallback = chatStateCallback; + this.resourceCallback = resourceCallback; } void _callback( @@ -41,11 +40,11 @@ class LocalLLMListener { ) { result.map((r) { switch (ty) { - case ChatNotification.UpdateChatPluginState: - stateCallback?.call(LocalAIPluginStatePB.fromBuffer(r)); + case ChatNotification.UpdateLocalAIState: + stateCallback?.call(LocalAIPB.fromBuffer(r)); break; - case ChatNotification.UpdateLocalChatAI: - chatStateCallback?.call(LocalAIChatPB.fromBuffer(r)); + case ChatNotification.LocalAIResourceUpdated: + resourceCallback?.call(LackOfAIResourcePB.fromBuffer(r)); break; default: break; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart new file mode 100644 index 0000000000..6c9ebe2a3f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart @@ -0,0 +1,221 @@ +import 'dart:async'; + +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:equatable/equatable.dart'; +part 'ollama_setting_bloc.freezed.dart'; + +class OllamaSettingBloc extends Bloc { + OllamaSettingBloc() : super(const OllamaSettingState()) { + on(_handleEvent); + } + + Future _handleEvent( + OllamaSettingEvent event, + Emitter emit, + ) async { + event.when( + started: () { + AIEventGetLocalAISetting().send().fold( + (setting) { + if (!isClosed) { + add(OllamaSettingEvent.didLoadSetting(setting)); + } + }, + Log.error, + ); + }, + didLoadSetting: (setting) => _updateSetting(setting, emit), + updateSetting: (setting) => _updateSetting(setting, emit), + onEdit: (content, settingType) { + final updatedSubmittedItems = state.submittedItems + .map( + (item) => item.settingType == settingType + ? SubmittedItem( + content: content, + settingType: item.settingType, + ) + : item, + ) + .toList(); + + // Convert both lists to maps: {settingType: content} + final updatedMap = { + for (final item in updatedSubmittedItems) + item.settingType: item.content, + }; + + final inputMap = { + for (final item in state.inputItems) item.settingType: item.content, + }; + + // Compare maps instead of lists + final isEdited = !const MapEquality() + .equals(updatedMap, inputMap); + + emit( + state.copyWith( + submittedItems: updatedSubmittedItems, + isEdited: isEdited, + ), + ); + }, + submit: () { + final setting = LocalAISettingPB(); + final settingUpdaters = { + SettingType.serverUrl: (value) => setting.serverUrl = value, + SettingType.chatModel: (value) => setting.chatModelName = value, + SettingType.embeddingModel: (value) => + setting.embeddingModelName = value, + }; + + for (final item in state.submittedItems) { + settingUpdaters[item.settingType]?.call(item.content); + } + add(OllamaSettingEvent.updateSetting(setting)); + AIEventUpdateLocalAISetting(setting).send().fold( + (_) { + Log.info('AI setting updated successfully'); + }, + (err) => Log.error("update ai setting failed: $err"), + ); + }, + ); + } + + void _updateSetting( + LocalAISettingPB setting, + Emitter emit, + ) { + emit( + state.copyWith( + setting: setting, + inputItems: _createInputItems(setting), + submittedItems: _createSubmittedItems(setting), + isEdited: false, // Reset to false when the setting is loaded/updated. + ), + ); + } + + List _createInputItems(LocalAISettingPB setting) => [ + SettingItem( + content: setting.serverUrl, + hintText: 'http://localhost:11434', + settingType: SettingType.serverUrl, + ), + SettingItem( + content: setting.chatModelName, + hintText: 'llama3.1', + settingType: SettingType.chatModel, + ), + SettingItem( + content: setting.embeddingModelName, + hintText: 'nomic-embed-text', + settingType: SettingType.embeddingModel, + ), + ]; + + List _createSubmittedItems(LocalAISettingPB setting) => [ + SubmittedItem( + content: setting.serverUrl, + settingType: SettingType.serverUrl, + ), + SubmittedItem( + content: setting.chatModelName, + settingType: SettingType.chatModel, + ), + SubmittedItem( + content: setting.embeddingModelName, + settingType: SettingType.embeddingModel, + ), + ]; +} + +// Create an enum for setting type. +enum SettingType { + serverUrl, + chatModel, + embeddingModel; // semicolon needed after the enum values + + String get title { + switch (this) { + case SettingType.serverUrl: + return 'Ollama server url'; + case SettingType.chatModel: + return 'Chat model name'; + case SettingType.embeddingModel: + return 'Embedding model name'; + } + } +} + +class SettingItem extends Equatable { + const SettingItem({ + required this.content, + required this.hintText, + required this.settingType, + }); + final String content; + final String hintText; + final SettingType settingType; + @override + List get props => [content, settingType]; +} + +class SubmittedItem extends Equatable { + const SubmittedItem({ + required this.content, + required this.settingType, + }); + final String content; + final SettingType settingType; + + @override + List get props => [content, settingType]; +} + +@freezed +class OllamaSettingEvent with _$OllamaSettingEvent { + const factory OllamaSettingEvent.started() = _Started; + const factory OllamaSettingEvent.didLoadSetting(LocalAISettingPB setting) = + _DidLoadSetting; + const factory OllamaSettingEvent.updateSetting(LocalAISettingPB setting) = + _UpdateSetting; + const factory OllamaSettingEvent.onEdit( + String content, + SettingType settingType, + ) = _OnEdit; + const factory OllamaSettingEvent.submit() = _OnSubmit; +} + +@freezed +class OllamaSettingState with _$OllamaSettingState { + const factory OllamaSettingState({ + LocalAISettingPB? setting, + @Default([ + SettingItem( + content: 'http://localhost:11434', + hintText: 'http://localhost:11434', + settingType: SettingType.serverUrl, + ), + SettingItem( + content: 'llama3.1', + hintText: 'llama3.1', + settingType: SettingType.chatModel, + ), + SettingItem( + content: 'nomic-embed-text', + hintText: 'nomic-embed-text', + settingType: SettingType.embeddingModel, + ), + ]) + List inputItems, + @Default([]) List submittedItems, + @Default(false) bool isEdited, + }) = _PluginStateState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart index 4f24309bde..93b0417e63 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -16,13 +15,18 @@ class PluginStateBloc extends Bloc { : listener = LocalLLMListener(), super( const PluginStateState( - action: PluginStateAction.init(), + action: PluginStateAction.unknown(), ), ) { listener.start( stateCallback: (pluginState) { if (!isClosed) { - add(PluginStateEvent.updateState(pluginState)); + add(PluginStateEvent.updateLocalAIState(pluginState)); + } + }, + resourceCallback: (data) { + if (!isClosed) { + add(PluginStateEvent.resourceStateChange(data)); } }, ); @@ -44,61 +48,70 @@ class PluginStateBloc extends Bloc { ) async { await event.when( started: () async { - final result = await AIEventGetLocalAIPluginState().send(); + final result = await AIEventGetLocalAIState().send(); result.fold( (pluginState) { if (!isClosed) { - add(PluginStateEvent.updateState(pluginState)); + add(PluginStateEvent.updateLocalAIState(pluginState)); } }, (err) => Log.error(err.toString()), ); }, - updateState: (LocalAIPluginStatePB pluginState) { + updateLocalAIState: (LocalAIPB aiState) { // if the offline ai is not started, ask user to start it - if (pluginState.offlineAiReady) { + if (aiState.isAppDownloaded) { + if (aiState.hasLackOfResource()) { + emit( + PluginStateState( + action: + PluginStateAction.lackOfResource(aiState.lackOfResource), + ), + ); + return; + } + // Chech state of the plugin - switch (pluginState.state) { + switch (aiState.state) { + case RunningStatePB.ReadyToRun: + emit( + const PluginStateState( + action: PluginStateAction.readToRun(), + ), + ); + case RunningStatePB.Connecting: emit( const PluginStateState( - action: PluginStateAction.loadingPlugin(), + action: PluginStateAction.initializingPlugin(), ), ); case RunningStatePB.Running: - emit(const PluginStateState(action: PluginStateAction.ready())); + emit(const PluginStateState(action: PluginStateAction.running())); break; - default: + case RunningStatePB.Stopped: emit( state.copyWith(action: const PluginStateAction.restartPlugin()), ); + default: break; } } else { emit( const PluginStateState( - action: PluginStateAction.startAIOfflineApp(), + action: PluginStateAction.downloadLocalAIApp(), ), ); } }, restartLocalAI: () async { emit( - const PluginStateState(action: PluginStateAction.loadingPlugin()), - ); - unawaited(AIEventRestartLocalAIChat().send()); - }, - openModelDirectory: () async { - final result = await AIEventGetModelStorageDirectory().send(); - result.fold( - (data) { - afLaunchUri(Uri.file(data.filePath)); - }, - (err) => Log.error(err.toString()), + const PluginStateState(action: PluginStateAction.restartPlugin()), ); + unawaited(AIEventRestartLocalAI().send()); }, downloadOfflineAIApp: () async { - final result = await AIEventGetOfflineAIAppLink().send(); + final result = await AIEventGetLocalAIDownloadLink().send(); await result.fold( (app) async { await launchUrl(Uri.parse(app.link)); @@ -106,6 +119,13 @@ class PluginStateBloc extends Bloc { (err) {}, ); }, + resourceStateChange: (data) { + emit( + PluginStateState( + action: PluginStateAction.lackOfResource(data.resourceDesc), + ), + ); + }, ); } } @@ -113,12 +133,12 @@ class PluginStateBloc extends Bloc { @freezed class PluginStateEvent with _$PluginStateEvent { const factory PluginStateEvent.started() = _Started; - const factory PluginStateEvent.updateState(LocalAIPluginStatePB pluginState) = - _UpdatePluginState; + const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) = + _UpdateLocalAIState; const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; - const factory PluginStateEvent.openModelDirectory() = - _OpenModelStorageDirectory; const factory PluginStateEvent.downloadOfflineAIApp() = _DownloadOfflineAIApp; + const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) = + _ResourceStateChange; } @freezed @@ -130,9 +150,11 @@ class PluginStateState with _$PluginStateState { @freezed class PluginStateAction with _$PluginStateAction { - const factory PluginStateAction.init() = _Init; - const factory PluginStateAction.loadingPlugin() = _LoadingPlugin; - const factory PluginStateAction.ready() = _Ready; + const factory PluginStateAction.unknown() = _Unknown; + const factory PluginStateAction.readToRun() = _ReadyToRun; + const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; + const factory PluginStateAction.running() = _PluginRunning; const factory PluginStateAction.restartPlugin() = _RestartPlugin; - const factory PluginStateAction.startAIOfflineApp() = _StartAIOfflineApp; + const factory PluginStateAction.downloadLocalAIApp() = _DownloadLocalAIApp; + const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart deleted file mode 100644 index a7ed782aea..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:percent_indicator/linear_percent_indicator.dart'; - -class DownloadingIndicator extends StatelessWidget { - const DownloadingIndicator({ - required this.llmModel, - required this.onCancel, - required this.onFinish, - super.key, - }); - final LLMModelPB llmModel; - final VoidCallback onCancel; - final VoidCallback onFinish; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()), - child: BlocListener( - listener: (context, state) { - if (state.isFinish) { - onFinish(); - } - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DownloadingProgressBar(onCancel: onCancel), - if (state.bigFileDownloadPrompt != null) ...[ - const VSpace(2), - Opacity( - opacity: 0.6, - child: - FlowyText(state.bigFileDownloadPrompt!, fontSize: 11), - ), - ], - ], - ); - }, - ), - ), - ), - ); - } -} - -class DownloadingProgressBar extends StatelessWidget { - const DownloadingProgressBar({required this.onCancel, super.key}); - - final VoidCallback onCancel; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.6, - child: FlowyText( - "${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}", - fontSize: 11, - ), - ), - IntrinsicHeight( - child: Row( - children: [ - Expanded( - child: LinearPercentIndicator( - lineHeight: 9.0, - percent: state.percent, - padding: EdgeInsets.zero, - progressColor: AFThemeExtension.of(context).success, - backgroundColor: - AFThemeExtension.of(context).progressBarBGColor, - barRadius: const Radius.circular(8), - trailing: FlowyText( - "${(state.percent * 100).toStringAsFixed(0)}%", - fontSize: 11, - color: AFThemeExtension.of(context).success, - ), - ), - ), - const HSpace(12), - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 11, - ), - onTap: onCancel, - ), - ], - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart index d924b46825..3f557eff91 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -22,7 +22,7 @@ class InitLocalAIIndicator extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { switch (state.runningState) { case RunningStatePB.Connecting: @@ -31,7 +31,7 @@ class InitLocalAIIndicator extends StatelessWidget { children: [ const HSpace(8), FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoading.tr(), + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), fontSize: 11, color: const Color(0xFF1E4620), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart deleted file mode 100644 index 9cb3a17d88..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart +++ /dev/null @@ -1,370 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:expandable/expandable.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalAIChatSetting extends StatelessWidget { - const LocalAIChatSetting({super.key}); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => LocalAIChatSettingBloc()), - BlocProvider( - create: (context) => LocalAIChatToggleBloc() - ..add(const LocalAIChatToggleEvent.started()), - ), - ], - child: ExpandableNotifier( - child: BlocListener( - listener: (context, state) { - // Listen to the toggle state and expand the panel if the state is ready. - final controller = ExpandableController.of( - context, - required: true, - )!; - - // Neet to wrap with WidgetsBinding.instance.addPostFrameCallback otherwise the - // ExpandablePanel not expanded sometimes. Maybe because the ExpandablePanel is not - // built yet when the listener is called. - WidgetsBinding.instance.addPostFrameCallback( - (_) { - state.pageIndicator.when( - error: (_) => controller.expanded = false, - ready: (enabled) { - controller.expanded = enabled; - context.read().add( - const LocalAIChatSettingEvent.refreshAISetting(), - ); - }, - loading: () => controller.expanded = false, - ); - }, - debugLabel: 'LocalAI.showLocalAIChatSetting', - ); - }, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: const SizedBox.shrink(), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - // child: _LocalLLMInfoWidget(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BlocBuilder( - builder: (context, state) { - // If the progress indicator is startOfflineAIApp, then don't show the LLM model. - if (state.progressIndicator == - const LocalAIProgress.startOfflineAIApp()) { - return const SizedBox.shrink(); - } else { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: FlowyText.medium( - LocaleKeys.settings_aiPage_keys_llmModel.tr(), - fontSize: 14, - ), - ), - const Spacer(), - state.aiModelProgress.when( - init: () => const SizedBox.shrink(), - loading: () { - return const Expanded( - child: Row( - children: [ - Spacer(), - CircularProgressIndicator.adaptive(), - ], - ), - ); - }, - finish: (err) => (err == null) - ? const _SelectLocalModelDropdownMenu() - : const SizedBox.shrink(), - ), - ], - ); - } - }, - ), - const IntrinsicHeight(child: _LocalAIStateWidget()), - ], - ), - ), - ), - ), - ), - ); - } -} - -class LocalAIChatSettingHeader extends StatelessWidget { - const LocalAIChatSettingHeader({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.pageIndicator.when( - error: (error) { - return const SizedBox.shrink(); - }, - loading: () { - return Row( - children: [ - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - ), - const Spacer(), - const CircularProgressIndicator.adaptive(), - const HSpace(8), - ], - ); - }, - ready: (isEnabled) { - return Row( - children: [ - const FlowyText('Enable Local AI Chat'), - const Spacer(), - Toggle( - value: isEnabled, - onChanged: (_) { - context - .read() - .add(const LocalAIChatToggleEvent.toggle()); - }, - ), - ], - ); - }, - ); - }, - ); - } -} - -class _SelectLocalModelDropdownMenu extends StatelessWidget { - const _SelectLocalModelDropdownMenu(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Flexible( - child: SettingsDropdown( - key: const Key('_SelectLocalModelDropdownMenu'), - onChanged: (model) => context.read().add( - LocalAIChatSettingEvent.selectLLMConfig(model), - ), - selectedOption: state.selectedLLMModel!, - options: state.models - .map( - (llm) => buildDropdownMenuEntry( - context, - value: llm, - label: llm.chatModel, - ), - ) - .toList(), - ), - ); - }, - ); - } -} - -class _LocalAIStateWidget extends StatelessWidget { - const _LocalAIStateWidget(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final error = errorFromState(state); - if (error == null) { - // If the error is null, handle selected llm model. - if (state.progressIndicator != null) { - final child = state.progressIndicator!.when( - showDownload: ( - LocalModelResourcePB llmResource, - LLMModelPB llmModel, - ) => - _ShowDownloadIndicator( - llmResource: llmResource, - llmModel: llmModel, - ), - startDownloading: (llmModel) { - return DownloadingIndicator( - key: UniqueKey(), - llmModel: llmModel, - onFinish: () => context - .read() - .add(const LocalAIChatSettingEvent.finishDownload()), - onCancel: () => context - .read() - .add(const LocalAIChatSettingEvent.cancelDownload()), - ); - }, - finishDownload: () => const InitLocalAIIndicator(), - checkPluginState: () => const PluginStateIndicator(), - startOfflineAIApp: () => OpenOrDownloadOfflineAIApp( - onRetry: () { - context - .read() - .add(const LocalAIChatSettingEvent.refreshAISetting()); - }, - ), - ); - - return Padding( - padding: const EdgeInsets.only(top: 8), - child: child, - ); - } else { - return const SizedBox.shrink(); - } - } else { - return Opacity( - opacity: 0.5, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: FlowyText( - error.msg, - maxLines: 10, - ), - ), - ); - } - }, - ); - } - - FlowyError? errorFromState(LocalAIChatSettingState state) { - final err = state.aiModelProgress.when( - loading: () => null, - finish: (err) => err, - init: () {}, - ); - - if (err == null) { - state.selectLLMState.when( - loading: () => null, - finish: (err) => err, - ); - } - - return err; - } -} - -void _showDownloadDialog( - BuildContext context, - LocalModelResourcePB llmResource, - LLMModelPB llmModel, -) { - if (llmResource.pendingResources.isEmpty) { - return; - } - - final res = llmResource.pendingResources.first; - String desc = ""; - switch (res.resType) { - case PendingResourceTypePB.AIModel: - desc = LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr( - args: [ - llmResource.pendingResources[0].name, - llmResource.pendingResources[0].fileSize, - ], - ); - break; - case PendingResourceTypePB.OfflineApp: - desc = LocaleKeys.settings_aiPage_keys_downloadAppFlowyOfflineAI.tr(); - break; - } - - showConfirmDialog( - context: context, - style: ConfirmPopupStyle.cancelAndOk, - title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr( - args: [res.name], - ), - description: desc, - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () => context.read().add( - LocalAIChatSettingEvent.startDownloadModel( - llmModel, - ), - ), - onCancel: () => context.read().add( - const LocalAIChatSettingEvent.cancelDownload(), - ), - ); -} - -class _ShowDownloadIndicator extends StatelessWidget { - const _ShowDownloadIndicator({ - required this.llmResource, - required this.llmModel, - }); - final LocalModelResourcePB llmResource; - final LLMModelPB llmModel; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - const Spacer(), - IntrinsicWidth( - child: SizedBox( - height: 30, - child: FlowyButton( - text: FlowyText( - LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(), - fontSize: 14, - color: const Color(0xFF005483), - ), - leftIcon: const FlowySvg( - FlowySvgs.local_model_download_s, - color: Color(0xFF005483), - ), - onTap: () { - _showDownloadDialog(context, llmResource, llmModel); - }, - ), - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index f9dc6fa4d8..706ecdd1a1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -68,7 +68,7 @@ class _LocalAISettingState extends State { child: const Padding( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: LocalAIChatSetting(), + child: LocalAISettingPanel(), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart new file mode 100644 index 0000000000..66379d347a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -0,0 +1,125 @@ +import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'plugin_state.dart'; + +class LocalAISettingPanel extends StatelessWidget { + const LocalAISettingPanel({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => LocalAISettingPanelBloc()), + BlocProvider( + create: (context) => LocalAIChatToggleBloc() + ..add(const LocalAIChatToggleEvent.started()), + ), + ], + child: ExpandableNotifier( + child: BlocListener( + listener: (context, state) { + // Listen to the toggle state and expand the panel if the state is ready. + final controller = ExpandableController.of( + context, + required: true, + )!; + + // Neet to wrap with WidgetsBinding.instance.addPostFrameCallback otherwise the + // ExpandablePanel not expanded sometimes. Maybe because the ExpandablePanel is not + // built yet when the listener is called. + WidgetsBinding.instance.addPostFrameCallback( + (_) { + state.pageIndicator.when( + error: (_) => controller.expanded = false, + ready: (enabled) { + controller.expanded = enabled; + context.read().add( + const LocalAISettingPanelEvent.started(), + ); + }, + loading: () => controller.expanded = false, + ); + }, + debugLabel: 'LocalAI.showLocalAIChatSetting', + ); + }, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const SizedBox.shrink(), + collapsed: const SizedBox.shrink(), + expanded: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + // child: _LocalLLMInfoWidget(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocBuilder( + builder: (context, state) { + // If the progress indicator is startLocalAIApp, then don't show the LLM model. + if (state.progressIndicator == + const LocalAIProgress.downloadLocalAIApp()) { + return const SizedBox.shrink(); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + OllamaSettingPage(), + VSpace(6), + _LocalAIStateWidget(), + ], + ); + } + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _LocalAIStateWidget extends StatelessWidget { + const _LocalAIStateWidget(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.progressIndicator != null) { + final child = state.progressIndicator!.when( + downloadLocalAIApp: () => OpenOrDownloadOfflineAIApp( + onRetry: () { + context + .read() + .add(const LocalAISettingPanelEvent.started()); + }, + ), + checkPluginState: () => const PluginStateIndicator(), + ); + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: child, + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart new file mode 100644 index 0000000000..45394a9db5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart @@ -0,0 +1,165 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OllamaSettingPage extends StatelessWidget { + const OllamaSettingPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + OllamaSettingBloc()..add(const OllamaSettingEvent.started()), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.inputItems != current.inputItems || + previous.isEdited != current.isEdited, + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _InstallOllamaInstruction(), + const VSpace(12), + ListView.separated( + shrinkWrap: true, + itemCount: state.inputItems.length, + separatorBuilder: (_, __) => const VSpace(10), + itemBuilder: (context, index) { + final item = state.inputItems[index]; + return _SettingItemWidget(item: item); + }, + ), + const VSpace(6), + _SaveButton(isEdited: state.isEdited), + ], + ); + }, + ), + ); + } +} + +class _SettingItemWidget extends StatelessWidget { + const _SettingItemWidget({required this.item}); + final SettingItem item; + + @override + Widget build(BuildContext context) { + return Column( + key: ValueKey(item.content + item.settingType.title), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText(item.settingType.title), + const VSpace(8), + FlowyTextField( + hintText: item.hintText, + text: item.content, + onChanged: (content) { + context.read().add( + OllamaSettingEvent.onEdit(content, item.settingType), + ); + }, + ), + ], + ); + } +} + +class _SaveButton extends StatelessWidget { + const _SaveButton({required this.isEdited}); + + final bool isEdited; + + @override + Widget build(BuildContext context) { + final tooltipMessage = isEdited ? 'Click to apply changes' : 'No changes'; + return SizedBox( + height: 50, + child: Row( + children: [ + const Spacer(), + SizedBox( + width: 120, + child: FlowyTooltip( + message: tooltipMessage, + child: Opacity( + opacity: isEdited ? 1 : 0.5, + child: FlowyTextButton( + 'Apply', + mainAxisAlignment: MainAxisAlignment.center, + onPressed: isEdited + ? () { + context + .read() + .add(const OllamaSettingEvent.submit()); + } + : null, + ), + ), + ), + ), + ], + ), + ); + } +} + +class _InstallOllamaInstruction extends StatelessWidget { + const _InstallOllamaInstruction(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: 3, + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString( + "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-ollama", + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + ], + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index bf601b6184..f1896b145b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -23,17 +24,19 @@ class PluginStateIndicator extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return state.action.when( - init: () => const _InitPlugin(), - ready: () => const _LocalAIReadyToUse(), - restartPlugin: () => const _ReloadButton(), - loadingPlugin: () => const _InitPlugin(), - startAIOfflineApp: () => OpenOrDownloadOfflineAIApp( + unknown: () => const SizedBox.shrink(), + readToRun: () => const SizedBox.shrink(), + initializingPlugin: () => const InitLocalAIIndicator(), + running: () => const _LocalAIRunning(), + restartPlugin: () => const _RestartPluginButton(), + downloadLocalAIApp: () => OpenOrDownloadOfflineAIApp( onRetry: () { context .read() .add(const PluginStateEvent.started()); }, ), + lackOfResource: (desc) => _LackOfResource(desc: desc), ); }, ), @@ -41,26 +44,8 @@ class PluginStateIndicator extends StatelessWidget { } } -class _InitPlugin extends StatelessWidget { - const _InitPlugin(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()), - const Spacer(), - const SizedBox( - height: 20, - child: CircularProgressIndicator.adaptive(), - ), - ], - ); - } -} - -class _ReloadButton extends StatelessWidget { - const _ReloadButton(); +class _RestartPluginButton extends StatelessWidget { + const _RestartPluginButton(); @override Widget build(BuildContext context) { @@ -91,8 +76,8 @@ class _ReloadButton extends StatelessWidget { } } -class _LocalAIReadyToUse extends StatelessWidget { - const _LocalAIReadyToUse(); +class _LocalAIRunning extends StatelessWidget { + const _LocalAIRunning(); @override Widget build(BuildContext context) { @@ -119,7 +104,7 @@ class _LocalAIReadyToUse extends StatelessWidget { const HSpace(6), Flexible( child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), fontSize: 11, color: const Color(0xFF1E4620), maxLines: 3, @@ -128,22 +113,6 @@ class _LocalAIReadyToUse extends StatelessWidget { ], ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.settings_aiPage_keys_openModelDirectory.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - onTap: () { - context.read().add( - const PluginStateEvent.openModelDirectory(), - ); - }, - ), - ), ], ), ), @@ -232,20 +201,6 @@ class OpenOrDownloadOfflineAIApp extends StatelessWidget { ], ), ), - // const SizedBox( - // height: 6, - // ), // Replaced VSpace with SizedBox for simplicity - // SizedBox( - // height: 30, - // child: FlowyButton( - // useIntrinsicWidth: true, - // margin: const EdgeInsets.symmetric(horizontal: 12), - // text: FlowyText( - // LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(), - // ), - // onTap: onRetry, - // ), - // ), ], ); }, @@ -253,3 +208,14 @@ class OpenOrDownloadOfflineAIApp extends StatelessWidget { ); } } + +class _LackOfResource extends StatelessWidget { + const _LackOfResource({required this.desc}); + + final String desc; + + @override + Widget build(BuildContext context) { + return FlowyText(desc); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3a78ef813f..e6a5658865 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -850,6 +850,8 @@ "localAIStart": "Local AI Chat is starting...", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", + "localAIRunning": "Local AI is running", + "localAIInitializing": "Local AI is initializing...", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", @@ -863,7 +865,10 @@ "offlineAIDownload3": "it first", "activeOfflineAI": "Active", "downloadOfflineAI": "Download", - "openModelDirectory": "Open folder" + "openModelDirectory": "Open folder", + "localAISetupInstruction1": "Follow the", + "localAISetupInstruction2": "instruction", + "localAISetupInstruction3": "to setup your Ollama" } }, "planPage": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 50cfd1cc7e..ddd730c1c3 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,11 +198,12 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=99447e23d5563328e5a2c2b0749e44540359a818#99447e23d5563328e5a2c2b0749e44540359a818" dependencies = [ "anyhow", "appflowy-plugin", "bytes", + "futures", "reqwest 0.11.27", "serde", "serde_json", @@ -217,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=99447e23d5563328e5a2c2b0749e44540359a818#99447e23d5563328e5a2c2b0749e44540359a818" dependencies = [ "anyhow", "cfg-if", @@ -1395,7 +1396,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -2725,9 +2726,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -2740,9 +2741,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2750,15 +2751,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -2767,9 +2768,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -2786,9 +2787,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -2797,21 +2798,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -4612,7 +4613,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4632,6 +4633,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4699,6 +4701,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 27c81a9f96..c73ac79d72 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -79,7 +79,7 @@ diesel = { version = "2.1.0", features = [ ] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" -futures = "0.3.29" +futures = "0.3.31" tokio = "1.38.0" tokio-stream = "0.1.14" async-trait = "0.1.81" @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "99447e23d5563328e5a2c2b0749e44540359a818" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "99447e23d5563328e5a2c2b0749e44540359a818" } diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index ab375c77fc..b817d639f1 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -22,7 +22,7 @@ tracing.workspace = true async-trait.workspace = true tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } -futures = "0.3" +futures = "0.3.31" arc-swap = "1.7" flowy-sqlite = { workspace = true } diesel.workspace = true diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 8ad17c028f..969f64e6f9 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -44,7 +44,7 @@ collab-integrate = { workspace = true } flowy-derive.workspace = true serde_yaml = "0.9.27" flowy-error = { workspace = true, features = ["impl_from_sqlite", "impl_from_dispatch_error", "impl_from_appflowy_cloud", "impl_from_reqwest", "impl_from_serde", "dart"] } -futures = "0.3.26" +futures = "0.3.31" [features] default = ["dart"] diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 392d82d858..21d94ae28a 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -55,7 +55,7 @@ tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" zip.workspace = true walkdir = "2.5.0" -futures = "0.3.30" +futures = "0.3.31" flowy-ai-pub = { workspace = true } [features] diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 5a784f6db6..7850d8767b 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -3,7 +3,7 @@ use crate::entities::{ ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::local_ai::local_llm_chat::LocalAIController; +use crate::local_ai::controller::LocalAIController; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; @@ -58,7 +58,7 @@ pub struct AIManager { pub user_service: Arc, pub external_service: Arc, chats: Arc>>, - pub local_ai_controller: Arc, + pub local_ai: Arc, store_preferences: Arc, } @@ -72,19 +72,23 @@ impl AIManager { ) -> AIManager { let user_service = Arc::new(user_service); let plugin_manager = Arc::new(PluginManager::new()); - let local_ai_controller = Arc::new(LocalAIController::new( + let local_ai = Arc::new(LocalAIController::new( plugin_manager.clone(), store_preferences.clone(), user_service.clone(), chat_cloud_service.clone(), )); - let external_service = Arc::new(query_service); - // setup local chat service + let cloned_local_ai = local_ai.clone(); + tokio::task::spawn_blocking(move || { + futures::executor::block_on(cloned_local_ai.init_plugin_when_first_run()); + }); + + let external_service = Arc::new(query_service); let cloud_service_wm = Arc::new(AICloudServiceMiddleware::new( user_service.clone(), chat_cloud_service, - local_ai_controller.clone(), + local_ai.clone(), storage_service, )); @@ -92,15 +96,14 @@ impl AIManager { cloud_service_wm, user_service, chats: Arc::new(DashMap::new()), - local_ai_controller, + local_ai, external_service, store_preferences, } } pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { - // Ignore following error - let _ = self.local_ai_controller.refresh().await; + let _ = self.local_ai.reload().await; Ok(()) } @@ -113,9 +116,9 @@ impl AIManager { self.cloud_service_wm.clone(), )) }); - trace!("[AI Plugin] notify open chat: {}", chat_id); - if self.local_ai_controller.is_running() { - self.local_ai_controller.open_chat(chat_id); + if self.local_ai.is_running() { + trace!("[AI Plugin] notify open chat: {}", chat_id); + self.local_ai.open_chat(chat_id); } let user_service = self.user_service.clone(); @@ -146,7 +149,7 @@ impl AIManager { pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("close chat: {}", chat_id); - self.local_ai_controller.close_chat(chat_id); + self.local_ai.close_chat(chat_id); Ok(()) } @@ -154,9 +157,9 @@ impl AIManager { if let Some((_, chat)) = self.chats.remove(chat_id) { chat.close(); - if self.local_ai_controller.is_running() { + if self.local_ai.is_running() { info!("[AI Plugin] notify close chat: {}", chat_id); - self.local_ai_controller.close_chat(chat_id); + self.local_ai.close_chat(chat_id); } } Ok(()) diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 7b1cb66593..f86cf4c743 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -1,8 +1,8 @@ -use crate::local_ai::local_llm_chat::LLMModelInfo; use appflowy_plugin::core::plugin::RunningState; use std::collections::HashMap; -use crate::local_ai::local_llm_resource::PendingResource; +use crate::local_ai::controller::LocalAISetting; +use crate::local_ai::resource::PendingResource; use flowy_ai_pub::cloud::{ ChatMessage, ChatMessageMetadata, ChatMessageType, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, @@ -309,24 +309,6 @@ impl From for RepeatedRelatedQuestionPB { } } -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct LLMModelInfoPB { - #[pb(index = 1)] - pub selected_model: LLMModelPB, - - #[pb(index = 2)] - pub models: Vec, -} - -impl From for LLMModelInfoPB { - fn from(value: LLMModelInfo) -> Self { - LLMModelInfoPB { - selected_model: LLMModelPB::from(value.selected_model), - models: value.models.into_iter().map(LLMModelPB::from).collect(), - } - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct LLMModelPB { #[pb(index = 1)] @@ -423,17 +405,6 @@ pub struct ChatFilePB { pub chat_id: String, } -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct DownloadLLMPB { - #[pb(index = 1)] - pub progress_stream: i64, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct DownloadTaskPB { - #[pb(index = 1)] - pub task_id: String, -} #[derive(Default, ProtoBuf, Clone, Debug)] pub struct LocalModelStatePB { #[pb(index = 1)] @@ -452,18 +423,6 @@ pub struct LocalModelStatePB { pub is_downloading: bool, } -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalModelResourcePB { - #[pb(index = 1)] - pub is_ready: bool, - - #[pb(index = 2)] - pub pending_resources: Vec, - - #[pb(index = 3)] - pub is_downloading: bool, -} - #[derive(Default, ProtoBuf, Clone, Debug)] pub struct PendingResourcePB { #[pb(index = 1)] @@ -482,40 +441,33 @@ pub struct PendingResourcePB { #[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] pub enum PendingResourceTypePB { #[default] - OfflineApp = 0, + LocalAIAppRes = 0, AIModel = 1, } impl From for PendingResourceTypePB { fn from(value: PendingResource) -> Self { match value { - PendingResource::OfflineApp { .. } => PendingResourceTypePB::OfflineApp, - PendingResource::ModelInfoRes { .. } => PendingResourceTypePB::AIModel, + PendingResource::LocalAIAppNotDownloaded { .. } => PendingResourceTypePB::LocalAIAppRes, + _ => PendingResourceTypePB::AIModel, } } } -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalAIPluginStatePB { - #[pb(index = 1)] - pub state: RunningStatePB, - - #[pb(index = 2)] - pub offline_ai_ready: bool, -} - #[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] pub enum RunningStatePB { #[default] - Connecting = 0, - Connected = 1, - Running = 2, - Stopped = 3, + ReadyToRun = 0, + Connecting = 1, + Connected = 2, + Running = 3, + Stopped = 4, } impl From for RunningStatePB { fn from(value: RunningState) -> Self { match value { + RunningState::ReadyToConnect => RunningStatePB::ReadyToRun, RunningState::Connecting => RunningStatePB::Connecting, RunningState::Connected { .. } => RunningStatePB::Connected, RunningState::Running { .. } => RunningStatePB::Running, @@ -529,28 +481,19 @@ impl From for RunningStatePB { pub struct LocalAIPB { #[pb(index = 1)] pub enabled: bool, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalAIChatPB { - #[pb(index = 1)] - pub enabled: bool, #[pb(index = 2)] - pub file_enabled: bool, + pub is_app_downloaded: bool, - #[pb(index = 3)] - pub plugin_state: LocalAIPluginStatePB, + #[pb(index = 3, one_of)] + pub lack_of_resource: Option, + + #[pb(index = 4)] + pub state: RunningStatePB, } #[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalModelStoragePB { - #[pb(index = 1)] - pub file_path: String, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct OfflineAIPB { +pub struct LocalAIAppLinkPB { #[pb(index = 1)] pub link: String, } @@ -636,3 +579,44 @@ impl From for ResponseFormat { } } } + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct LocalAISettingPB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub server_url: String, + + #[pb(index = 2)] + #[validate(custom(function = "required_not_empty_str"))] + pub chat_model_name: String, + + #[pb(index = 3)] + #[validate(custom(function = "required_not_empty_str"))] + pub embedding_model_name: String, +} + +impl From for LocalAISettingPB { + fn from(value: LocalAISetting) -> Self { + LocalAISettingPB { + server_url: value.ollama_server_url, + chat_model_name: value.chat_model_name, + embedding_model_name: value.embedding_model_name, + } + } +} + +impl From for LocalAISetting { + fn from(value: LocalAISettingPB) -> Self { + LocalAISetting { + ollama_server_url: value.server_url, + chat_model_name: value.chat_model_name, + embedding_model_name: value.embedding_model_name, + } + } +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LackOfAIResourcePB { + #[pb(index = 1)] + pub resource_desc: String, +} diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 746a843da5..5a8018ae4b 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -4,15 +4,9 @@ use std::path::PathBuf; use crate::ai_manager::AIManager; use crate::completion::AICompletion; use crate::entities::*; -use crate::local_ai::local_llm_chat::LLMModelInfo; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use lib_infra::isolate_stream::IsolateSink; use serde_json::json; use std::sync::{Arc, Weak}; use tracing::trace; @@ -109,7 +103,7 @@ pub(crate) async fn regenerate_response_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_available_model_list_handler( +pub(crate) async fn get_model_list_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; @@ -193,47 +187,6 @@ pub(crate) async fn stop_stream_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn refresh_local_ai_info_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let model_info = ai_manager.local_ai_controller.refresh_model_info().await; - if model_info.is_err() { - if let Some(llm_model) = ai_manager.local_ai_controller.get_current_model() { - let model_info = LLMModelInfo { - selected_model: llm_model.clone(), - models: vec![llm_model], - }; - return data_result_ok(model_info.into()); - } - } - data_result_ok(model_info?.into()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_local_llm_model_handler( - data: AFPluginData, - ai_manager: AFPluginState>, -) -> DataResult { - let data = data.into_inner(); - let ai_manager = upgrade_ai_manager(ai_manager)?; - let state = ai_manager - .local_ai_controller - .select_local_llm(data.llm_id) - .await?; - data_result_ok(state) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_local_llm_state_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let state = ai_manager.local_ai_controller.get_local_llm_state().await?; - data_result_ok(state) -} - pub(crate) async fn start_complete_text_handler( data: AFPluginData, tools: AFPluginState>, @@ -301,108 +254,11 @@ pub(crate) async fn chat_file_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn download_llm_resource_handler( - data: AFPluginData, - ai_manager: AFPluginState>, -) -> DataResult { - let data = data.into_inner(); - let ai_manager = upgrade_ai_manager(ai_manager)?; - let text_sink = IsolateSink::new(Isolate::new(data.progress_stream)); - let task_id = ai_manager - .local_ai_controller - .start_downloading(text_sink) - .await?; - data_result_ok(DownloadTaskPB { task_id }) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn cancel_download_llm_resource_handler( +pub(crate) async fn restart_local_ai_handler( ai_manager: AFPluginState>, ) -> Result<(), FlowyError> { let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.local_ai_controller.cancel_download()?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_plugin_state_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let state = ai_manager.local_ai_controller.get_chat_plugin_state(); - data_result_ok(state) -} -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn toggle_local_ai_chat_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let enabled = ai_manager - .local_ai_controller - .toggle_local_ai_chat() - .await?; - let file_enabled = ai_manager.local_ai_controller.is_rag_enabled(); - let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); - let pb = LocalAIChatPB { - enabled, - file_enabled, - plugin_state, - }; - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalChatAI, - ) - .payload(pb.clone()) - .send(); - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn toggle_local_ai_chat_file_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let enabled = ai_manager.local_ai_controller.is_chat_enabled(); - let file_enabled = ai_manager - .local_ai_controller - .toggle_local_ai_chat_rag() - .await?; - let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); - let pb = LocalAIChatPB { - enabled, - file_enabled, - plugin_state, - }; - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalChatAI, - ) - .payload(pb.clone()) - .send(); - - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_local_ai_chat_state_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let enabled = ai_manager.local_ai_controller.is_chat_enabled(); - let file_enabled = ai_manager.local_ai_controller.is_rag_enabled(); - let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); - data_result_ok(LocalAIChatPB { - enabled, - file_enabled, - plugin_state, - }) -} -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn restart_local_ai_chat_handler( - ai_manager: AFPluginState>, -) -> Result<(), FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.local_ai_controller.restart_chat_plugin(); + ai_manager.local_ai.restart_plugin().await; Ok(()) } @@ -411,8 +267,9 @@ pub(crate) async fn toggle_local_ai_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let enabled = ai_manager.local_ai_controller.toggle_local_ai().await?; - data_result_ok(LocalAIPB { enabled }) + let _ = ai_manager.local_ai.toggle_local_ai().await?; + let state = ai_manager.local_ai.get_local_ai_state().await; + data_result_ok(state) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -420,31 +277,17 @@ pub(crate) async fn get_local_ai_state_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let enabled = ai_manager.local_ai_controller.is_enabled(); - data_result_ok(LocalAIPB { enabled }) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_model_storage_directory_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let file_path = ai_manager - .local_ai_controller - .get_model_storage_directory()?; - data_result_ok(LocalModelStoragePB { file_path }) + let state = ai_manager.local_ai.get_local_ai_state().await; + data_result_ok(state) } #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_offline_app_handler( ai_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let link = ai_manager - .local_ai_controller - .get_offline_ai_app_download_link() - .await?; - data_result_ok(OfflineAIPB { link }) + let link = ai_manager.local_ai.get_plugin_download_link().await?; + data_result_ok(LocalAIAppLinkPB { link }) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -493,3 +336,27 @@ pub(crate) async fn update_chat_settings_handler( Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_local_ai_setting_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let setting = ai_manager.local_ai.get_local_ai_setting(); + let pb = LocalAISettingPB::from(setting); + data_result_ok(pb) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn update_local_ai_setting_handler( + ai_manager: AFPluginState>, + data: AFPluginData, +) -> Result<(), FlowyError> { + let data = data.try_into_inner()?; + let ai_manager = upgrade_ai_manager(ai_manager)?; + ai_manager + .local_ai + .update_local_ai_setting(data.into()) + .await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 2bd2bee863..7875e7c3c9 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -23,47 +23,24 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::GetRelatedQuestion, get_related_question_handler) .event(AIEvent::GetAnswerForQuestion, get_answer_handler) .event(AIEvent::StopStream, stop_stream_handler) - .event( - AIEvent::RefreshLocalAIModelInfo, - refresh_local_ai_info_handler, - ) - .event(AIEvent::UpdateLocalLLM, update_local_llm_model_handler) - .event(AIEvent::GetLocalLLMState, get_local_llm_state_handler) .event(AIEvent::CompleteText, start_complete_text_handler) .event(AIEvent::StopCompleteText, stop_complete_text_handler) .event(AIEvent::ChatWithFile, chat_file_handler) - .event(AIEvent::DownloadLLMResource, download_llm_resource_handler) - .event( - AIEvent::CancelDownloadLLMResource, - cancel_download_llm_resource_handler, - ) - .event(AIEvent::GetLocalAIPluginState, get_plugin_state_handler) - .event(AIEvent::ToggleLocalAIChat, toggle_local_ai_chat_handler) - .event( - AIEvent::GetLocalAIChatState, - get_local_ai_chat_state_handler, - ) - .event(AIEvent::RestartLocalAIChat, restart_local_ai_chat_handler) + .event(AIEvent::RestartLocalAI, restart_local_ai_handler) .event(AIEvent::ToggleLocalAI, toggle_local_ai_handler) .event(AIEvent::GetLocalAIState, get_local_ai_state_handler) + .event(AIEvent::GetLocalAIDownloadLink, get_offline_app_handler) + .event(AIEvent::GetLocalAISetting, get_local_ai_setting_handler) .event( - AIEvent::ToggleChatWithFile, - toggle_local_ai_chat_file_handler, + AIEvent::UpdateLocalAISetting, + update_local_ai_setting_handler, ) - .event( - AIEvent::GetModelStorageDirectory, - get_model_storage_directory_handler, - ) - .event(AIEvent::GetOfflineAIAppLink, get_offline_app_handler) + .event(AIEvent::GetAvailableModels, get_model_list_handler) .event(AIEvent::CreateChatContext, create_chat_context_handler) .event(AIEvent::GetChatInfo, create_chat_context_handler) .event(AIEvent::GetChatSettings, get_chat_settings_handler) .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) .event(AIEvent::RegenerateResponse, regenerate_response_handler) - .event( - AIEvent::GetAvailableModels, - get_available_model_list_handler, - ) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -88,15 +65,6 @@ pub enum AIEvent { #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] GetAnswerForQuestion = 5, - #[event(input = "LLMModelPB", output = "LocalModelResourcePB")] - UpdateLocalLLM = 6, - - #[event(output = "LocalModelResourcePB")] - GetLocalLLMState = 7, - - #[event(output = "LLMModelInfoPB")] - RefreshLocalAIModelInfo = 8, - #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] CompleteText = 9, @@ -106,26 +74,10 @@ pub enum AIEvent { #[event(input = "ChatFilePB")] ChatWithFile = 11, - #[event(input = "DownloadLLMPB", output = "DownloadTaskPB")] - DownloadLLMResource = 12, - - #[event()] - CancelDownloadLLMResource = 13, - - #[event(output = "LocalAIPluginStatePB")] - GetLocalAIPluginState = 14, - - #[event(output = "LocalAIChatPB")] - ToggleLocalAIChat = 15, - - /// Return Local AI Chat State - #[event(output = "LocalAIChatPB")] - GetLocalAIChatState = 16, - /// Restart local AI chat. When plugin quit or user terminate in task manager or activity monitor, /// the plugin will need to restart. #[event()] - RestartLocalAIChat = 17, + RestartLocalAI = 17, /// Enable or disable local AI #[event(output = "LocalAIPB")] @@ -135,14 +87,8 @@ pub enum AIEvent { #[event(output = "LocalAIPB")] GetLocalAIState = 19, - #[event()] - ToggleChatWithFile = 20, - - #[event(output = "LocalModelStoragePB")] - GetModelStorageDirectory = 21, - - #[event(output = "OfflineAIPB")] - GetOfflineAIAppLink = 22, + #[event(output = "LocalAIAppLinkPB")] + GetLocalAIDownloadLink = 22, #[event(input = "CreateChatContextPB")] CreateChatContext = 23, @@ -161,4 +107,10 @@ pub enum AIEvent { #[event(output = "ModelConfigPB")] GetAvailableModels = 28, + + #[event(output = "LocalAISettingPB")] + GetLocalAISetting = 29, + + #[event(input = "LocalAISettingPB")] + UpdateLocalAISetting = 30, } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs similarity index 50% rename from frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs rename to frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index ece0bdddda..c44b62be01 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -1,17 +1,12 @@ use crate::ai_manager::AIUserService; -use crate::entities::{LocalAIPluginStatePB, LocalModelResourcePB, RunningStatePB}; -use crate::local_ai::local_llm_resource::{LLMResourceService, LocalAIResourceController}; +use crate::entities::{LackOfAIResourcePB, LocalAIPB, RunningStatePB}; +use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use anyhow::Error; -use appflowy_local_ai::chat_plugin::{AIPluginConfig, AppFlowyLocalAI}; use appflowy_plugin::manager::PluginManager; -use appflowy_plugin::util::is_apple_silicon; -use flowy_ai_pub::cloud::{ - AppFlowyOfflineAI, ChatCloudService, ChatMessageMetadata, ContextLoader, LLMModel, LocalAIConfig, - SubscriptionPlan, -}; +use flowy_ai_pub::cloud::{ChatCloudService, ChatMessageMetadata, ContextLoader, LocalAIConfig}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; @@ -19,8 +14,10 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use crate::stream_message::StreamMessage; +use appflowy_local_ai::ollama_plugin::OllamaAIPlugin; use arc_swap::ArcSwapOption; use futures_util::SinkExt; +use lib_infra::util::get_operating_system; use serde::{Deserialize, Serialize}; use serde_json::json; use std::ops::Deref; @@ -28,38 +25,43 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, trace, warn}; +use tracing::{debug, error, info, instrument, trace}; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LLMSetting { - pub app: AppFlowyOfflineAI, - pub llm_model: LLMModel, +pub struct LocalAISetting { + pub ollama_server_url: String, + pub chat_model_name: String, + pub embedding_model_name: String, } -pub struct LLMModelInfo { - pub selected_model: LLMModel, - pub models: Vec, +impl Default for LocalAISetting { + fn default() -> Self { + Self { + ollama_server_url: "http://localhost:11434".to_string(), + chat_model_name: "llama3.1".to_string(), + embedding_model_name: "nomic-embed-text".to_string(), + } + } } const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; -const APPFLOWY_LOCAL_AI_CHAT_ENABLED: &str = "appflowy_local_ai_chat_enabled"; -const APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED: &str = "appflowy_local_ai_chat_rag_enabled"; -const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v0"; +const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1"; pub struct LocalAIController { - local_ai: Arc, - local_ai_resource: Arc, + ai_plugin: Arc, + resource: Arc, current_chat_id: ArcSwapOption, store_preferences: Arc, user_service: Arc, + #[allow(dead_code)] cloud_service: Arc, } impl Deref for LocalAIController { - type Target = Arc; + type Target = Arc; fn deref(&self) -> &Self::Target { - &self.local_ai + &self.ai_plugin } } @@ -70,128 +72,112 @@ impl LocalAIController { user_service: Arc, cloud_service: Arc, ) -> Self { - let local_ai = Arc::new(AppFlowyLocalAI::new(plugin_manager)); + let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), cloud_service: cloud_service.clone(), store_preferences: store_preferences.clone(), }; - let (tx, mut rx) = tokio::sync::mpsc::channel(1); - let llm_res = Arc::new(LocalAIResourceController::new( + let local_ai_resource = Arc::new(LocalAIResourceController::new( user_service.clone(), res_impl, - tx, )); let current_chat_id = ArcSwapOption::default(); - let mut running_state_rx = local_ai.subscribe_running_state(); - let cloned_llm_res = llm_res.clone(); + let cloned_llm_res = local_ai_resource.clone(); + let cloned_store_preferences = store_preferences.clone(); tokio::spawn(async move { while let Some(state) = running_state_rx.next().await { info!("[AI Plugin] state: {:?}", state); - let offline_ai_ready = cloned_llm_res.is_offline_app_ready(); + let ready = cloned_llm_res.is_app_downloaded(); + let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; + let new_state = RunningStatePB::from(state); + let enabled = cloned_store_preferences + .get_bool(APPFLOWY_LOCAL_AI_ENABLED) + .unwrap_or(true); chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateChatPluginState, + ChatNotification::UpdateLocalAIState, ) - .payload(LocalAIPluginStatePB { + .payload(LocalAIPB { + enabled, + is_app_downloaded: ready, + lack_of_resource, state: new_state, - offline_ai_ready, }) .send(); } }); - let this = Self { - local_ai, - local_ai_resource: llm_res, + Self { + ai_plugin: local_ai, + resource: local_ai_resource, current_chat_id, store_preferences, user_service, cloud_service, - }; + } + } - let rag_enabled = this.is_rag_enabled(); - let cloned_llm_chat = this.local_ai.clone(); - let cloned_llm_res = this.local_ai_resource.clone(); - let mut offline_ai_watch = this.local_ai_resource.subscribe_offline_app_state(); + #[instrument(level = "debug", skip_all)] + pub async fn init_plugin_when_first_run(&self) { + debug!( + "[AI Plugin] init plugin when first run. thread: {:?}", + std::thread::current().id() + ); + let sys = get_operating_system(); + if !sys.is_desktop() { + return; + } + async fn try_init_plugin( + resource: &Arc, + ai_plugin: &Arc, + ) { + if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None).await { + error!("[AI Plugin] failed to setup plugin: {:?}", err); + } + } + + // Clone what is needed for the background task. + let resource_clone = self.resource.clone(); + let ai_plugin_clone = self.ai_plugin.clone(); + let mut resource_notify = self.resource.subscribe_resource_notify(); + let mut app_state_watcher = self.resource.subscribe_app_state(); tokio::spawn(async move { - let init_fn = || { - if let Ok(chat_config) = cloned_llm_res.get_chat_config(rag_enabled) { - if let Err(err) = initialize_ai_plugin(&cloned_llm_chat, chat_config, None) { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } - }; - loop { select! { - _ = offline_ai_watch.recv() => { - init_fn(); - }, - _ = rx.recv() => { - init_fn(); - }, - else => { break; } + _ = app_state_watcher.recv() => { + info!("[AI Plugin] app state changed, try to init plugin"); + try_init_plugin(&resource_clone, &ai_plugin_clone).await; + }, + _ = resource_notify.recv() => { + info!("[AI Plugin] resource changed, try to init plugin"); + try_init_plugin(&resource_clone, &ai_plugin_clone).await; + }, + else => break, } } }); - if this.can_init_plugin() { - let result = this - .local_ai_resource - .get_chat_config(this.is_rag_enabled()); - if let Ok(chat_config) = result { - if let Err(err) = initialize_ai_plugin(&this.local_ai, chat_config, None) { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } + // Perform initial initialization if required. + if self.should_init_plugin().await { + try_init_plugin(&self.resource, &self.ai_plugin).await; + } else { + trace!("[AI Plugin] local ai is able to start"); } - - this } - pub async fn refresh(&self) -> FlowyResult<()> { + + pub async fn reload(&self) -> FlowyResult<()> { let is_enabled = self.is_enabled(); - self.enable_chat_plugin(is_enabled).await?; - - if is_enabled { - let local_ai = self.local_ai.clone(); - let workspace_id = self.user_service.workspace_id()?; - let cloned_service = self.cloud_service.clone(); - let store_preferences = self.store_preferences.clone(); - tokio::spawn(async move { - let key = local_ai_enabled_key(&workspace_id); - match cloned_service.get_workspace_plan(&workspace_id).await { - Ok(plans) => { - trace!("[AI Plugin] workspace:{} plans: {:?}", workspace_id, plans); - if !plans.contains(&SubscriptionPlan::AiLocal) { - info!( - "disable local ai plugin for workspace: {}. reason: no plan found", - workspace_id - ); - let _ = store_preferences.set_bool(&key, false); - let _ = local_ai.destroy_chat_plugin().await; - } - }, - Err(err) => { - warn!("[AI Plugin]: failed to get workspace plan: {:?}", err); - }, - } - }); - } - + self.toggle_plugin(is_enabled).await?; Ok(()) } - pub async fn refresh_model_info(&self) -> FlowyResult { - self.local_ai_resource.refresh_llm_resource().await - } - /// Returns true if the local AI is enabled and ready to use. - pub fn can_init_plugin(&self) -> bool { - self.is_enabled() && self.local_ai_resource.is_resource_ready() + pub async fn should_init_plugin(&self) -> bool { + self.is_enabled() && self.resource.is_resource_ready().await } /// Indicate whether the local AI plugin is running. @@ -199,7 +185,7 @@ impl LocalAIController { if !self.is_enabled() { return false; } - self.local_ai.get_plugin_running_state().is_ready() + self.ai_plugin.get_plugin_running_state().is_ready() } /// Indicate whether the local AI is enabled. @@ -217,22 +203,6 @@ impl LocalAIController { } } - /// Indicate whether the local AI chat is enabled. In the future, we can support multiple - /// AI plugin. - pub fn is_chat_enabled(&self) -> bool { - self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) - .unwrap_or(true) - } - - pub fn is_rag_enabled(&self) -> bool { - self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED) - .unwrap_or(true) - } - pub fn open_chat(&self, chat_id: &str) { if !self.is_enabled() { return; @@ -249,7 +219,7 @@ impl LocalAIController { .current_chat_id .store(Some(Arc::new(chat_id.to_string()))); let chat_id = chat_id.to_string(); - let weak_ctrl = Arc::downgrade(&self.local_ai); + let weak_ctrl = Arc::downgrade(&self.ai_plugin); tokio::spawn(async move { if let Some(ctrl) = weak_ctrl.upgrade() { if let Err(err) = ctrl.create_chat(&chat_id).await { @@ -264,7 +234,7 @@ impl LocalAIController { return; } info!("[AI Plugin] notify close chat: {}", chat_id); - let weak_ctrl = Arc::downgrade(&self.local_ai); + let weak_ctrl = Arc::downgrade(&self.ai_plugin); let chat_id = chat_id.to_string(); tokio::spawn(async move { if let Some(ctrl) = weak_ctrl.upgrade() { @@ -275,91 +245,51 @@ impl LocalAIController { }); } - pub async fn select_local_llm(&self, llm_id: i64) -> FlowyResult { - if !self.is_enabled() { - return Err(FlowyError::local_ai_unavailable()); - } - - if let Some(model) = self.local_ai_resource.get_selected_model() { - if model.llm_id == llm_id { - return self.local_ai_resource.get_local_llm_state(); - } - } - - let state = self.local_ai_resource.use_local_llm(llm_id)?; - // Re-initialize the plugin if the setting is updated and ready to use - if self.local_ai_resource.is_resource_ready() { - let chat_config = self - .local_ai_resource - .get_chat_config(self.is_rag_enabled())?; - if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, None) { - error!("failed to setup plugin: {:?}", err); - } - } - Ok(state) + pub fn get_local_ai_setting(&self) -> LocalAISetting { + self.resource.get_llm_setting() } - pub async fn get_local_llm_state(&self) -> FlowyResult { - self.local_ai_resource.get_local_llm_state() - } - - pub fn get_current_model(&self) -> Option { - self.local_ai_resource.get_selected_model() - } - - pub async fn start_downloading(&self, progress_sink: T) -> FlowyResult - where - T: Sink + Unpin + Sync + Send + 'static, - { - let task_id = self - .local_ai_resource - .start_downloading(progress_sink) - .await?; - Ok(task_id) - } - - pub fn cancel_download(&self) -> FlowyResult<()> { - self.local_ai_resource.cancel_download()?; + pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { + info!( + "[AI Plugin] update local ai setting: {:?}, thread: {:?}", + setting, + std::thread::current().id() + ); + self.destroy_chat_plugin().await?; + self.resource.set_llm_setting(setting).await?; + self.reload().await?; Ok(()) } - pub fn get_chat_plugin_state(&self) -> LocalAIPluginStatePB { - if !self.is_enabled() { - return LocalAIPluginStatePB { - state: RunningStatePB::Stopped, - offline_ai_ready: false, - }; - } - - let offline_ai_ready = self.local_ai_resource.is_offline_app_ready(); - let state = self.local_ai.get_plugin_running_state(); - LocalAIPluginStatePB { + pub async fn get_local_ai_state(&self) -> LocalAIPB { + let enabled = self.is_enabled(); + let is_app_downloaded = self.resource.is_app_downloaded(); + let state = self.ai_plugin.get_plugin_running_state(); + let lack_of_resource = self.resource.get_lack_of_resource().await; + LocalAIPB { + enabled, + is_app_downloaded, state: RunningStatePB::from(state), - offline_ai_ready, + lack_of_resource, } } - pub fn restart_chat_plugin(&self) { - let rag_enabled = self.is_rag_enabled(); - if let Ok(chat_config) = self.local_ai_resource.get_chat_config(rag_enabled) { - if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, None) { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } + #[instrument(level = "debug", skip_all)] + pub async fn restart_plugin(&self) { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { + error!("[AI Plugin] failed to setup plugin: {:?}", err); } } pub fn get_model_storage_directory(&self) -> FlowyResult { self - .local_ai_resource + .resource .user_model_folder() .map(|path| path.to_string_lossy().to_string()) } - pub async fn get_offline_ai_app_download_link(&self) -> FlowyResult { - self - .local_ai_resource - .get_offline_ai_app_download_link() - .await + pub async fn get_plugin_download_link(&self) -> FlowyResult { + self.resource.get_plugin_download_link().await } pub async fn toggle_local_ai(&self) -> FlowyResult { @@ -368,45 +298,13 @@ impl LocalAIController { let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); self.store_preferences.set_bool(&key, enabled)?; - // when enable local ai. we need to check if chat is enabled, if enabled, we need to init chat plugin - // otherwise, we need to destroy the plugin - if enabled { - let chat_enabled = self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) - .unwrap_or(true); - - if self.local_ai_resource.is_resource_ready() { - self.enable_chat_plugin(chat_enabled).await?; - } - } else { - let _ = self.enable_chat_plugin(false).await; + if self.resource.is_resource_ready().await { + self.toggle_plugin(enabled).await?; } - Ok(enabled) - } - - pub async fn toggle_local_ai_chat(&self) -> FlowyResult { - let enabled = !self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) - .unwrap_or(true); - self - .store_preferences - .set_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED, enabled)?; - self.enable_chat_plugin(enabled).await?; Ok(enabled) } - pub async fn toggle_local_ai_chat_rag(&self) -> FlowyResult { - let enabled = !self - .store_preferences - .get_bool_or_default(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED); - self - .store_preferences - .set_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED, enabled)?; - Ok(enabled) - } pub async fn index_message_metadata( &self, chat_id: &str, @@ -521,18 +419,20 @@ impl LocalAIController { Ok(()) } - async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> { - info!("[AI Plugin] enable chat plugin: {}", enabled); + #[instrument(level = "debug", skip_all)] + async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> { + info!( + "[AI Plugin] enable: {}, thread id: {:?}", + enabled, + std::thread::current().id() + ); if enabled { let (tx, rx) = tokio::sync::oneshot::channel(); - let chat_config = self - .local_ai_resource - .get_chat_config(self.is_rag_enabled())?; - if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, Some(tx)) { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx)).await { error!("[AI Plugin] failed to initialize local ai: {:?}", err); } let _ = rx.await; - } else if let Err(err) = self.local_ai.destroy_chat_plugin().await { + } else if let Err(err) = self.ai_plugin.destroy_chat_plugin().await { error!("[AI Plugin] failed to destroy plugin: {:?}", err); } Ok(()) @@ -540,27 +440,70 @@ impl LocalAIController { } #[instrument(level = "debug", skip_all, err)] -fn initialize_ai_plugin( - llm_chat: &Arc, - mut chat_config: AIPluginConfig, +async fn initialize_ai_plugin( + llm_chat: &Arc, + llm_resource: &Arc, ret: Option>, ) -> FlowyResult<()> { let llm_chat = llm_chat.clone(); + let lack_of_resource = llm_resource.get_lack_of_resource().await; + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled: true, + is_app_downloaded: true, + state: RunningStatePB::ReadyToRun, + lack_of_resource: lack_of_resource.clone(), + }) + .send(); - tokio::spawn(async move { - info!("[AI Plugin] config: {:?}", chat_config); - if is_apple_silicon().await.unwrap_or(false) { - chat_config = chat_config.with_device("gpu"); - } - match llm_chat.init_chat_plugin(chat_config).await { - Ok(_) => {}, - Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), - } + if let Some(lack_of_resource) = lack_of_resource { + info!( + "[AI Plugin] lack of resource: {:?} to initialize plugin, thread: {:?}", + lack_of_resource, + std::thread::current().id() + ); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::LocalAIResourceUpdated, + ) + .payload(LackOfAIResourcePB { + resource_desc: lack_of_resource, + }) + .send(); - if let Some(ret) = ret { - let _ = ret.send(()); - } + return Ok(()); + } + + let cloned_llm_res = llm_resource.clone(); + tokio::task::spawn_blocking(move || { + futures::executor::block_on(async move { + match cloned_llm_res.get_plugin_config(true).await { + Ok(config) => { + info!( + "[AI Plugin] initialize plugin with config: {:?}, thread: {:?}", + config, + std::thread::current().id() + ); + + match llm_chat.init_chat_plugin(config).await { + Ok(_) => {}, + Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), + } + + if let Some(ret) = ret { + let _ = ret.send(()); + } + }, + Err(err) => { + error!("[AI Plugin] failed to get plugin config: {:?}", err); + }, + }; + }) }); + Ok(()) } @@ -580,17 +523,17 @@ impl LLMResourceService for LLMResourceServiceImpl { Ok(config) } - fn store_setting(&self, setting: LLMSetting) -> Result<(), Error> { + fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { self .store_preferences .set_object(LOCAL_AI_SETTING_KEY, &setting)?; Ok(()) } - fn retrieve_setting(&self) -> Option { + fn retrieve_setting(&self) -> Option { self .store_preferences - .get_object::(LOCAL_AI_SETTING_KEY) + .get_object::(LOCAL_AI_SETTING_KEY) } } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs deleted file mode 100644 index 90dc328a6d..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs +++ /dev/null @@ -1,534 +0,0 @@ -use crate::ai_manager::AIUserService; -use crate::entities::{LocalModelResourcePB, PendingResourcePB, PendingResourceTypePB}; -use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; -use crate::local_ai::model_request::download_model; - -use appflowy_local_ai::chat_plugin::AIPluginConfig; -use flowy_ai_pub::cloud::{LLMModel, LocalAIConfig, ModelInfo}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use futures::Sink; -use futures_util::SinkExt; -use lib_infra::async_trait::async_trait; - -use arc_swap::ArcSwapOption; -use lib_infra::util::{get_operating_system, OperatingSystem}; -use std::path::PathBuf; -use std::sync::Arc; - -use crate::local_ai::watch::offline_app_path; -#[cfg(target_os = "macos")] -use crate::local_ai::watch::{watch_offline_app, WatchContext}; -use tokio::fs::{self}; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, instrument, trace, warn}; - -#[async_trait] -pub trait LLMResourceService: Send + Sync + 'static { - /// Get local ai configuration from remote server - async fn fetch_local_ai_config(&self) -> Result; - fn store_setting(&self, setting: LLMSetting) -> Result<(), anyhow::Error>; - fn retrieve_setting(&self) -> Option; -} - -const LLM_MODEL_DIR: &str = "models"; -const DOWNLOAD_FINISH: &str = "finish"; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum WatchDiskEvent { - Create, - Remove, -} - -pub enum PendingResource { - OfflineApp, - ModelInfoRes(Vec), -} -#[derive(Clone)] -pub struct DownloadTask { - cancel_token: CancellationToken, - tx: tokio::sync::broadcast::Sender, -} -impl DownloadTask { - pub fn new() -> Self { - let (tx, _) = tokio::sync::broadcast::channel(100); - let cancel_token = CancellationToken::new(); - Self { cancel_token, tx } - } - - pub fn cancel(&self) { - self.cancel_token.cancel(); - } -} - -pub struct LocalAIResourceController { - user_service: Arc, - resource_service: Arc, - llm_setting: ArcSwapOption, - // The ai_config will be set when user try to get latest local ai config from server - ai_config: ArcSwapOption, - download_task: Arc>, - resource_notify: tokio::sync::mpsc::Sender<()>, - #[cfg(target_os = "macos")] - #[allow(dead_code)] - offline_app_disk_watch: Option, - offline_app_state_sender: tokio::sync::broadcast::Sender, -} - -impl LocalAIResourceController { - pub fn new( - user_service: Arc, - resource_service: impl LLMResourceService, - resource_notify: tokio::sync::mpsc::Sender<()>, - ) -> Self { - let (offline_app_state_sender, _) = tokio::sync::broadcast::channel(1); - let llm_setting = resource_service.retrieve_setting().map(Arc::new); - #[cfg(target_os = "macos")] - let mut offline_app_disk_watch: Option = None; - - #[cfg(target_os = "macos")] - { - match watch_offline_app() { - Ok((new_watcher, mut rx)) => { - let sender = offline_app_state_sender.clone(); - tokio::spawn(async move { - while let Some(event) = rx.recv().await { - if let Err(err) = sender.send(event) { - error!("[LLM Resource] Failed to send offline app state: {:?}", err); - } - } - }); - offline_app_disk_watch = Some(new_watcher); - }, - Err(err) => { - error!("[LLM Resource] Failed to watch offline app path: {:?}", err); - }, - } - } - - Self { - user_service, - resource_service: Arc::new(resource_service), - llm_setting: ArcSwapOption::new(llm_setting), - ai_config: Default::default(), - download_task: Default::default(), - resource_notify, - #[cfg(target_os = "macos")] - offline_app_disk_watch, - offline_app_state_sender, - } - } - - #[allow(dead_code)] - pub fn subscribe_offline_app_state(&self) -> tokio::sync::broadcast::Receiver { - self.offline_app_state_sender.subscribe() - } - - fn set_llm_setting(&self, llm_setting: LLMSetting) { - self.llm_setting.store(Some(llm_setting.into())); - } - - /// Returns true when all resources are downloaded and ready to use. - pub fn is_resource_ready(&self) -> bool { - match self.calculate_pending_resources() { - Ok(res) => res.is_empty(), - Err(_) => false, - } - } - - pub fn is_offline_app_ready(&self) -> bool { - offline_app_path().exists() - } - - pub async fn get_offline_ai_app_download_link(&self) -> FlowyResult { - let ai_config = self.fetch_ai_config().await?; - Ok(ai_config.plugin.url) - } - - /// Retrieves model information and updates the current model settings. - #[instrument(level = "debug", skip_all, err)] - pub async fn refresh_llm_resource(&self) -> FlowyResult { - let ai_config = self.fetch_ai_config().await?; - if ai_config.models.is_empty() { - return Err(FlowyError::local_ai().with_context("No model found")); - } - - self.ai_config.store(Some(ai_config.clone().into())); - let selected_model = self.select_model(&ai_config)?; - - let llm_setting = LLMSetting { - app: ai_config.plugin.clone(), - llm_model: selected_model.clone(), - }; - self.set_llm_setting(llm_setting.clone()); - self.resource_service.store_setting(llm_setting)?; - - Ok(LLMModelInfo { - selected_model, - models: ai_config.models, - }) - } - - #[instrument(level = "info", skip_all, err)] - pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { - let (app, llm_model) = self - .ai_config - .load() - .as_ref() - .and_then(|config| { - config - .models - .iter() - .find(|model| model.llm_id == llm_id) - .cloned() - .map(|model| (config.plugin.clone(), model)) - }) - .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; - - let llm_setting = LLMSetting { - app, - llm_model: llm_model.clone(), - }; - - trace!("[LLM Resource] Selected AI setting: {:?}", llm_setting); - self.set_llm_setting(llm_setting.clone()); - self.resource_service.store_setting(llm_setting)?; - self.get_local_llm_state() - } - - pub fn get_local_llm_state(&self) -> FlowyResult { - let state = self - .check_resource() - .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; - Ok(state) - } - - #[instrument(level = "debug", skip_all)] - fn check_resource(&self) -> Option { - trace!("[LLM Resource] Checking local ai resources"); - - let pending_resources = self.calculate_pending_resources().ok()?; - let is_ready = pending_resources.is_empty(); - let is_downloading = self.download_task.load().is_some(); - let pending_resources: Vec<_> = pending_resources - .into_iter() - .flat_map(|res| match res { - PendingResource::OfflineApp => vec![PendingResourcePB { - name: "AppFlowy Plugin".to_string(), - file_size: "0 GB".to_string(), - requirements: "".to_string(), - res_type: PendingResourceTypePB::OfflineApp, - }], - PendingResource::ModelInfoRes(model_infos) => model_infos - .into_iter() - .map(|model_info| PendingResourcePB { - name: model_info.name, - file_size: bytes_to_readable_format(model_info.file_size as u64), - requirements: model_info.requirements, - res_type: PendingResourceTypePB::AIModel, - }) - .collect::>(), - }) - .collect(); - - let resource = LocalModelResourcePB { - is_ready, - pending_resources, - is_downloading, - }; - - debug!("[LLM Resource] Local AI resources state: {:?}", resource); - Some(resource) - } - - /// Returns true when all resources are downloaded and ready to use. - pub fn calculate_pending_resources(&self) -> FlowyResult> { - match self.llm_setting.load().as_ref() { - None => Err(FlowyError::local_ai().with_context("Can't find any llm config")), - Some(llm_setting) => { - let mut resources = vec![]; - let app_path = offline_app_path(); - if !app_path.exists() { - trace!("[LLM Resource] offline app not found: {:?}", app_path); - resources.push(PendingResource::OfflineApp); - } - - let chat_model = self.model_path(&llm_setting.llm_model.chat_model.file_name)?; - if !chat_model.exists() { - resources.push(PendingResource::ModelInfoRes(vec![llm_setting - .llm_model - .chat_model - .clone()])); - } - - let embedding_model = self.model_path(&llm_setting.llm_model.embedding_model.file_name)?; - if !embedding_model.exists() { - resources.push(PendingResource::ModelInfoRes(vec![llm_setting - .llm_model - .embedding_model - .clone()])); - } - - Ok(resources) - }, - } - } - - #[instrument(level = "info", skip_all, err)] - pub async fn start_downloading(&self, mut progress_sink: T) -> FlowyResult - where - T: Sink + Unpin + Sync + Send + 'static, - { - let task_id = uuid::Uuid::new_v4().to_string(); - let weak_download_task = Arc::downgrade(&self.download_task); - let resource_notify = self.resource_notify.clone(); - // notify download progress to client. - let progress_notify = |mut rx: tokio::sync::broadcast::Receiver| { - tokio::spawn(async move { - while let Ok(value) = rx.recv().await { - let is_finish = value == DOWNLOAD_FINISH; - if let Err(err) = progress_sink.send(value).await { - warn!("Failed to send progress: {:?}", err); - break; - } - - if is_finish { - info!("notify download finish, need to reload resources"); - let _ = resource_notify.send(()).await; - if let Some(download_task) = weak_download_task.upgrade() { - if let Some(task) = download_task.swap(None) { - task.cancel(); - } - } - break; - } - } - }); - }; - - // return immediately if download task already exists - { - let guard = self.download_task.load(); - if let Some(download_task) = &*guard { - trace!( - "Download task already exists, return the task id: {}", - task_id - ); - progress_notify(download_task.tx.subscribe()); - return Ok(task_id); - } - } - - // If download task is not exists, create a new download task. - info!("[LLM Resource] Start new download task"); - let llm_setting = self - .llm_setting - .load_full() - .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; - - let download_task = Arc::new(DownloadTask::new()); - self.download_task.store(Some(download_task.clone())); - progress_notify(download_task.tx.subscribe()); - - let model_dir = self.user_model_folder()?; - if !model_dir.exists() { - fs::create_dir_all(&model_dir).await.map_err(|err| { - FlowyError::local_ai().with_context(format!("Failed to create model dir: {:?}", err)) - })?; - } - - tokio::spawn(async move { - // After download the plugin, start downloading models - let chat_model_file = ( - model_dir.join(&llm_setting.llm_model.chat_model.file_name), - &llm_setting.llm_model.chat_model.file_name, - &llm_setting.llm_model.chat_model.name, - &llm_setting.llm_model.chat_model.download_url, - ); - let embedding_model_file = ( - model_dir.join(&llm_setting.llm_model.embedding_model.file_name), - &llm_setting.llm_model.embedding_model.file_name, - &llm_setting.llm_model.embedding_model.name, - &llm_setting.llm_model.embedding_model.download_url, - ); - for (file_path, file_name, model_name, url) in [chat_model_file, embedding_model_file] { - if file_path.exists() { - continue; - } - - info!("[LLM Resource] Downloading model: {:?}", file_name); - let plugin_progress_tx = download_task.tx.clone(); - let cloned_model_name = model_name.clone(); - let progress = Arc::new(move |downloaded, total_size| { - let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); - if plugin_progress_tx.receiver_count() == 0 { - return; - } - - if let Err(err) = - plugin_progress_tx.send(format!("{}:progress:{}", cloned_model_name, progress)) - { - warn!("Failed to send progress: {:?}", err); - } - }); - match download_model( - url, - &model_dir, - file_name, - Some(progress), - Some(download_task.cancel_token.clone()), - ) - .await - { - Ok(_) => info!("[LLM Resource] Downloaded model: {:?}", file_name), - Err(err) => { - error!( - "[LLM Resource] Failed to download model for given url: {:?}, error: {:?}", - url, err - ); - download_task - .tx - .send(format!("error:failed to download {}", model_name))?; - continue; - }, - } - } - info!("[LLM Resource] All resources downloaded"); - download_task.tx.send(DOWNLOAD_FINISH.to_string())?; - Ok::<_, anyhow::Error>(()) - }); - - Ok(task_id) - } - - pub fn cancel_download(&self) -> FlowyResult<()> { - if let Some(cancel_token) = self.download_task.swap(None) { - info!("[LLM Resource] Cancel download"); - cancel_token.cancel(); - } - - Ok(()) - } - - #[instrument(level = "info", skip_all)] - pub fn get_chat_config(&self, rag_enabled: bool) -> FlowyResult { - if !self.is_resource_ready() { - return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); - } - - let llm_setting = self - .llm_setting - .load_full() - .ok_or_else(|| FlowyError::local_ai().with_context("No local llm setting found"))?; - - let model_dir = self.user_model_folder()?; - let bin_path = match get_operating_system() { - OperatingSystem::MacOS => { - let path = offline_app_path(); - if !path.exists() { - return Err(FlowyError::new( - ErrorCode::AIOfflineNotInstalled, - format!("AppFlowy Offline not installed at path: {:?}", path), - )); - } - path - }, - _ => { - return Err( - FlowyError::local_ai_unavailable() - .with_context("Local AI not available on current platform"), - ); - }, - }; - - let chat_model_path = model_dir.join(&llm_setting.llm_model.chat_model.file_name); - let mut config = AIPluginConfig::new(bin_path, chat_model_path)?; - - if rag_enabled { - let resource_dir = self.resource_dir()?; - let embedding_model_path = model_dir.join(&llm_setting.llm_model.embedding_model.file_name); - let persist_directory = resource_dir.join("vectorstore"); - if !persist_directory.exists() { - std::fs::create_dir_all(&persist_directory)?; - } - config.set_rag_enabled(&embedding_model_path, &persist_directory)?; - } - - if cfg!(debug_assertions) { - config = config.with_verbose(true); - } - trace!("[AI Chat] use config: {:?}", config); - Ok(config) - } - - /// Fetches the local AI configuration from the resource service. - async fn fetch_ai_config(&self) -> FlowyResult { - self - .resource_service - .fetch_local_ai_config() - .await - .map_err(|err| { - error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); - FlowyError::local_ai() - .with_context("Can't retrieve model info. Please try again later".to_string()) - }) - } - - pub fn get_selected_model(&self) -> Option { - let setting = self.llm_setting.load(); - Some(setting.as_ref()?.llm_model.clone()) - } - - /// Selects the appropriate model based on the current settings or defaults to the first model. - fn select_model(&self, ai_config: &LocalAIConfig) -> FlowyResult { - let llm_setting = self.llm_setting.load(); - let selected_model = match &*llm_setting { - None => ai_config.models[0].clone(), - Some(llm_setting) => { - match ai_config - .models - .iter() - .find(|model| model.llm_id == llm_setting.llm_model.llm_id) - { - None => ai_config.models[0].clone(), - Some(llm_model) => { - if llm_model != &llm_setting.llm_model { - info!( - "[LLM Resource] existing model is different from remote, replace with remote model" - ); - } - llm_model.clone() - }, - } - }, - }; - Ok(selected_model) - } - - pub(crate) fn user_model_folder(&self) -> FlowyResult { - self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) - } - - fn model_path(&self, model_file_name: &str) -> FlowyResult { - self - .user_model_folder() - .map(|dir| dir.join(model_file_name)) - } - - pub(crate) fn resource_dir(&self) -> FlowyResult { - let user_data_dir = self.user_service.application_root_dir()?; - Ok(user_data_dir.join("ai")) - } -} -fn bytes_to_readable_format(bytes: u64) -> String { - const BYTES_IN_GIGABYTE: u64 = 1024 * 1024 * 1024; - const BYTES_IN_MEGABYTE: u64 = 1024 * 1024; - - if bytes >= BYTES_IN_GIGABYTE { - let gigabytes = (bytes as f64) / (BYTES_IN_GIGABYTE as f64); - format!("{:.1} GB", gigabytes) - } else { - let megabytes = (bytes as f64) / (BYTES_IN_MEGABYTE as f64); - format!("{:.2} MB", megabytes) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs b/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs index 5daddf881b..c0fd967d43 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs @@ -1,6 +1,6 @@ -pub mod local_llm_chat; -pub mod local_llm_resource; -mod model_request; +pub mod controller; +mod request; +pub mod resource; pub mod stream_util; pub mod watch; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs b/frontend/rust-lib/flowy-ai/src/local_ai/request.rs similarity index 99% rename from frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs rename to frontend/rust-lib/flowy-ai/src/local_ai/request.rs index c37a6f04ff..dd94b43041 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/request.rs @@ -12,6 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio_util::sync::CancellationToken; use tracing::{instrument, trace}; +#[allow(dead_code)] type ProgressCallback = Arc; #[instrument(level = "trace", skip_all, err)] @@ -95,6 +96,7 @@ pub async fn download_model( Ok(download_path) } +#[allow(dead_code)] async fn make_request( client: &Client, url: &str, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs new file mode 100644 index 0000000000..15bde0e431 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -0,0 +1,315 @@ +use crate::ai_manager::AIUserService; +use crate::local_ai::controller::LocalAISetting; +use flowy_ai_pub::cloud::LocalAIConfig; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use lib_infra::async_trait::async_trait; + +use crate::entities::LackOfAIResourcePB; +use crate::local_ai::watch::ollama_plugin_path; +#[cfg(target_os = "macos")] +use crate::local_ai::watch::{watch_offline_app, WatchContext}; +use crate::notification::{ + chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, +}; +use appflowy_local_ai::ollama_plugin::OllamaPluginConfig; +use lib_infra::util::{get_operating_system, OperatingSystem}; +use reqwest::Client; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tracing::{error, info, instrument, trace}; + +#[async_trait] +pub trait LLMResourceService: Send + Sync + 'static { + /// Get local ai configuration from remote server + async fn fetch_local_ai_config(&self) -> Result; + fn store_setting(&self, setting: LocalAISetting) -> Result<(), anyhow::Error>; + fn retrieve_setting(&self) -> Option; +} + +const LLM_MODEL_DIR: &str = "models"; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum WatchDiskEvent { + Create, + Remove, +} + +pub enum PendingResource { + LocalAIAppNotDownloaded, + OllamaServerNotFound, + OllamaNotInstalled, + MissingModel(String), +} + +impl PendingResource { + pub fn desc(self) -> String { + match self { + PendingResource::LocalAIAppNotDownloaded => "Local AI app not downloaded".to_string(), + PendingResource::OllamaServerNotFound => "Ollama server not ready".to_string(), + PendingResource::OllamaNotInstalled => "Ollama not installed".to_string(), + PendingResource::MissingModel(model) => format!("Missing model: {}", model), + } + } +} + +pub struct LocalAIResourceController { + user_service: Arc, + resource_service: Arc, + resource_notify: tokio::sync::broadcast::Sender<()>, + #[cfg(target_os = "macos")] + #[allow(dead_code)] + app_disk_watch: Option, + app_state_sender: tokio::sync::broadcast::Sender, +} + +impl LocalAIResourceController { + pub fn new( + user_service: Arc, + resource_service: impl LLMResourceService, + ) -> Self { + let (resource_notify, _) = tokio::sync::broadcast::channel(1); + let (app_state_sender, _) = tokio::sync::broadcast::channel(1); + #[cfg(target_os = "macos")] + let mut offline_app_disk_watch: Option = None; + + #[cfg(target_os = "macos")] + { + match watch_offline_app() { + Ok((new_watcher, mut rx)) => { + let sender = app_state_sender.clone(); + tokio::spawn(async move { + while let Some(event) = rx.recv().await { + if let Err(err) = sender.send(event) { + error!("[LLM Resource] Failed to send offline app state: {:?}", err); + } + } + }); + offline_app_disk_watch = Some(new_watcher); + }, + Err(err) => { + error!("[LLM Resource] Failed to watch offline app path: {:?}", err); + }, + } + } + + Self { + user_service, + resource_service: Arc::new(resource_service), + #[cfg(target_os = "macos")] + app_disk_watch: offline_app_disk_watch, + app_state_sender, + resource_notify, + } + } + + pub fn subscribe_resource_notify(&self) -> tokio::sync::broadcast::Receiver<()> { + self.resource_notify.subscribe() + } + + pub fn subscribe_app_state(&self) -> tokio::sync::broadcast::Receiver { + self.app_state_sender.subscribe() + } + + /// Returns true when all resources are downloaded and ready to use. + pub async fn is_resource_ready(&self) -> bool { + let sys = get_operating_system(); + if !sys.is_desktop() { + return false; + } + + match self.calculate_pending_resources().await { + Ok(res) => res.is_empty(), + Err(_) => false, + } + } + + pub fn is_app_downloaded(&self) -> bool { + ollama_plugin_path().exists() + } + + pub async fn get_plugin_download_link(&self) -> FlowyResult { + let ai_config = self.get_local_ai_configuration().await?; + Ok(ai_config.plugin.url) + } + + /// Retrieves model information and updates the current model settings. + pub fn get_llm_setting(&self) -> LocalAISetting { + self.resource_service.retrieve_setting().unwrap_or_default() + } + + #[instrument(level = "info", skip_all, err)] + pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { + self.resource_service.store_setting(setting)?; + let mut resources = self.calculate_pending_resources().await?; + if let Some(resource) = resources.pop() { + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::LocalAIResourceUpdated, + ) + .payload(LackOfAIResourcePB { + resource_desc: resource.desc(), + }) + .send(); + } + Ok(()) + } + + pub async fn get_lack_of_resource(&self) -> Option { + let mut resources = self.calculate_pending_resources().await.ok()?; + resources.pop().map(|r| r.desc()) + } + + /// Returns true when all resources are downloaded and ready to use. + pub async fn calculate_pending_resources(&self) -> FlowyResult> { + let mut resources = vec![]; + let app_path = ollama_plugin_path(); + if !app_path.exists() { + trace!("[LLM Resource] offline app not found: {:?}", app_path); + resources.push(PendingResource::LocalAIAppNotDownloaded); + } + + let setting = self.get_llm_setting(); + let client = Client::builder().timeout(Duration::from_secs(5)).build()?; + match client.get(&setting.ollama_server_url).send().await { + Ok(resp) if resp.status().is_success() => { + info!( + "[LLM Resource] Ollama server is running at {}", + setting.ollama_server_url + ); + }, + _ => { + info!( + "[LLM Resource] Ollama server is not responding at {}", + setting.ollama_server_url + ); + resources.push(PendingResource::OllamaServerNotFound); + return Ok(resources); + }, + } + + let required_models = vec![ + setting.chat_model_name, + setting.embedding_model_name, + // Add any additional required models here. + ]; + match tokio::process::Command::new("ollama") + .arg("list") + .output() + .await + { + Ok(output) if output.status.success() => { + let stdout = String::from_utf8_lossy(&output.stdout); + for model in &required_models { + if !stdout.contains(model.as_str()) { + log::trace!( + "[LLM Resource] required model '{}' not found in ollama list", + model + ); + resources.push(PendingResource::MissingModel(model.clone())); + return Ok(resources); + } + } + }, + Ok(output) => { + error!( + "[LLM Resource] 'ollama list' command failed with status: {:?}", + output.status + ); + resources.push(PendingResource::OllamaNotInstalled); + }, + Err(e) => { + error!("[LLM Resource] failed to execute 'ollama list': {:?}", e); + resources.push(PendingResource::OllamaNotInstalled); + }, + } + + Ok(resources) + } + + #[instrument(level = "info", skip_all)] + pub async fn get_plugin_config(&self, rag_enabled: bool) -> FlowyResult { + if !self.is_resource_ready().await { + return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); + } + + let llm_setting = self.get_llm_setting(); + let bin_path = match get_operating_system() { + OperatingSystem::MacOS => { + let path = ollama_plugin_path(); + if !path.exists() { + return Err(FlowyError::new( + ErrorCode::AIOfflineNotInstalled, + format!("AppFlowy Offline not installed at path: {:?}", path), + )); + } + path + }, + _ => { + return Err( + FlowyError::local_ai_unavailable() + .with_context("Local AI not available on current platform"), + ); + }, + }; + + let mut config = OllamaPluginConfig::new( + bin_path, + llm_setting.chat_model_name.clone(), + llm_setting.embedding_model_name.clone(), + Some(llm_setting.ollama_server_url.clone()), + )?; + + if rag_enabled { + let resource_dir = self.resource_dir()?; + let persist_directory = resource_dir.join("vectorstore"); + if !persist_directory.exists() { + std::fs::create_dir_all(&persist_directory)?; + } + config.set_rag_enabled(&persist_directory)?; + } + + if cfg!(debug_assertions) { + config = config.with_verbose(true); + } + trace!("[AI Chat] config: {:?}", config); + Ok(config) + } + + /// Fetches the local AI configuration from the resource service. + async fn get_local_ai_configuration(&self) -> FlowyResult { + self + .resource_service + .fetch_local_ai_config() + .await + .map_err(|err| { + error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); + FlowyError::local_ai() + .with_context("Can't retrieve model info. Please try again later".to_string()) + }) + } + + pub(crate) fn user_model_folder(&self) -> FlowyResult { + self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) + } + + pub(crate) fn resource_dir(&self) -> FlowyResult { + let user_data_dir = self.user_service.application_root_dir()?; + Ok(user_data_dir.join("ai")) + } +} + +#[allow(dead_code)] +fn bytes_to_readable_format(bytes: u64) -> String { + const BYTES_IN_GIGABYTE: u64 = 1024 * 1024 * 1024; + const BYTES_IN_MEGABYTE: u64 = 1024 * 1024; + + if bytes >= BYTES_IN_GIGABYTE { + let gigabytes = (bytes as f64) / (BYTES_IN_GIGABYTE as f64); + format!("{:.1} GB", gigabytes) + } else { + let megabytes = (bytes as f64) / (BYTES_IN_MEGABYTE as f64); + format!("{:.2} MB", megabytes) + } +} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index cee8f1d381..4c88354ba7 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -1,4 +1,4 @@ -use crate::local_ai::local_llm_resource::WatchDiskEvent; +use crate::local_ai::resource::WatchDiskEvent; use flowy_error::{FlowyError, FlowyResult}; use std::path::PathBuf; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; @@ -20,7 +20,7 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver| match res { Ok(event) => { if event.paths.iter().any(|path| path == &app_path) { @@ -81,8 +81,8 @@ pub(crate) fn offline_app_path() -> PathBuf { } #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn offline_app_path() -> PathBuf { - let offline_app = "appflowy_ai_plugin"; +pub(crate) fn ollama_plugin_path() -> PathBuf { + let offline_app = "ollama_ai_plugin"; #[cfg(target_os = "windows")] return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index d6dce3696c..6fb41c84f2 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -1,6 +1,6 @@ use crate::ai_manager::AIUserService; use crate::entities::{ChatStatePB, ModelTypePB}; -use crate::local_ai::local_llm_chat::LocalAIController; +use crate::local_ai::controller::LocalAIController; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; @@ -30,7 +30,7 @@ use tracing::trace; pub struct AICloudServiceMiddleware { cloud_service: Arc, user_service: Arc, - local_llm_controller: Arc, + local_ai: Arc, storage_service: Weak, } @@ -38,19 +38,19 @@ impl AICloudServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, - local_llm_controller: Arc, + local_ai: Arc, storage_service: Weak, ) -> Self { Self { user_service, cloud_service, - local_llm_controller, + local_ai, storage_service, } } pub fn is_local_ai_enabled(&self) -> bool { - self.local_llm_controller.is_enabled() + self.local_ai.is_enabled() } pub async fn index_message_metadata( @@ -68,7 +68,7 @@ impl AICloudServiceMiddleware { .await; self - .local_llm_controller + .local_ai .index_message_metadata(chat_id, metadata_list, index_process_sink) .await?; let _ = index_process_sink @@ -97,7 +97,7 @@ impl AICloudServiceMiddleware { ) { chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateChatPluginState, + ChatNotification::UpdateLocalAIState, ) .payload(ChatStatePB { model_type: ModelTypePB::LocalAI, @@ -158,11 +158,11 @@ impl ChatCloudService for AICloudServiceMiddleware { question_id: i64, format: ResponseFormat, ) -> Result { - if self.local_llm_controller.is_running() { + if self.local_ai.is_running() { let row = self.get_message_record(question_id)?; match self - .local_llm_controller - .stream_question(chat_id, &row.content, json!([])) + .local_ai + .stream_question(chat_id, &row.content, json!({})) .await { Ok(stream) => Ok(QuestionStream::new(stream).boxed()), @@ -185,13 +185,9 @@ impl ChatCloudService for AICloudServiceMiddleware { chat_id: &str, question_message_id: i64, ) -> Result { - if self.local_llm_controller.is_running() { + if self.local_ai.is_running() { let content = self.get_message_record(question_message_id)?.content; - match self - .local_llm_controller - .ask_question(chat_id, &content) - .await - { + match self.local_ai.ask_question(chat_id, &content).await { Ok(answer) => { // TODO(nathan): metadata let message = self @@ -244,9 +240,9 @@ impl ChatCloudService for AICloudServiceMiddleware { chat_id: &str, message_id: i64, ) -> Result { - if self.local_llm_controller.is_running() { + if self.local_ai.is_running() { let questions = self - .local_llm_controller + .local_ai .get_related_question(chat_id) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; @@ -274,9 +270,9 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &str, params: CompleteTextParams, ) -> Result { - if self.local_llm_controller.is_running() { + if self.local_ai.is_running() { match self - .local_llm_controller + .local_ai .complete_text(¶ms.text, params.completion_type.unwrap() as u8) .await { @@ -305,9 +301,9 @@ impl ChatCloudService for AICloudServiceMiddleware { chat_id: &str, metadata: Option>, ) -> Result<(), FlowyError> { - if self.local_llm_controller.is_running() { + if self.local_ai.is_running() { self - .local_llm_controller + .local_ai .index_file(chat_id, Some(file_path.to_path_buf()), None, metadata) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; diff --git a/frontend/rust-lib/flowy-ai/src/notification.rs b/frontend/rust-lib/flowy-ai/src/notification.rs index c7857dbc8a..81d1bb92d6 100644 --- a/frontend/rust-lib/flowy-ai/src/notification.rs +++ b/frontend/rust-lib/flowy-ai/src/notification.rs @@ -12,9 +12,9 @@ pub enum ChatNotification { DidReceiveChatMessage = 3, StreamChatMessageError = 4, FinishStreaming = 5, - UpdateChatPluginState = 6, - UpdateLocalChatAI = 7, - DidUpdateChatSettings = 8, + UpdateLocalAIState = 6, + DidUpdateChatSettings = 7, + LocalAIResourceUpdated = 8, } impl std::convert::From for i32 { @@ -30,8 +30,9 @@ impl std::convert::From for ChatNotification { 3 => ChatNotification::DidReceiveChatMessage, 4 => ChatNotification::StreamChatMessageError, 5 => ChatNotification::FinishStreaming, - 6 => ChatNotification::UpdateChatPluginState, - 7 => ChatNotification::UpdateLocalChatAI, + 6 => ChatNotification::UpdateLocalAIState, + 7 => ChatNotification::DidUpdateChatSettings, + 8 => ChatNotification::LocalAIResourceUpdated, _ => ChatNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 56ac310300..328b90aa15 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -51,10 +51,10 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware { object_id: &str, summary_row: SummaryRowContent, ) -> Result { - if self.ai_manager.local_ai_controller.is_running() { + if self.ai_manager.local_ai.is_running() { self .ai_manager - .local_ai_controller + .local_ai .summary_database_row(summary_row) .await .map_err(|err| FlowyError::local_ai().with_context(err)) @@ -72,7 +72,7 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware { translate_row: TranslateRowContent, language: &str, ) -> Result { - if self.ai_manager.local_ai_controller.is_running() { + if self.ai_manager.local_ai.is_running() { let data = LocalAITranslateRowData { cells: translate_row .into_iter() @@ -86,7 +86,7 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware { }; let resp = self .ai_manager - .local_ai_controller + .local_ai .translate_database_row(data) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 30d9850e55..1a36ecce2c 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -41,7 +41,7 @@ validator.workspace = true async-trait.workspace = true client-api = { workspace = true } regex = "1.9.5" -futures = "0.3.30" +futures = "0.3.31" dashmap.workspace = true diff --git a/frontend/rust-lib/lib-infra/Cargo.toml b/frontend/rust-lib/lib-infra/Cargo.toml index 12c805862b..a07a3413f4 100644 --- a/frontend/rust-lib/lib-infra/Cargo.toml +++ b/frontend/rust-lib/lib-infra/Cargo.toml @@ -22,7 +22,7 @@ validator = { workspace = true, features = ["derive"] } tracing.workspace = true atomic_refcell = "0.1" allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true } -futures = "0.3.30" +futures = "0.3.31" cfg-if = "1.0.0" futures-util = "0.3.30" @@ -36,7 +36,7 @@ base64 = { version = "0.22.1" } [dev-dependencies] rand = "0.8.5" -futures = "0.3.30" +futures = "0.3.31" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] zip = { version = "2.2.0", features = ["deflate"] } From addb041816d072756b90a94d3c796423d420a462 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 00:25:19 +0800 Subject: [PATCH 084/384] chore: update exe path --- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 4c88354ba7..d18e045cbe 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -81,14 +81,25 @@ pub(crate) fn offline_app_path() -> PathBuf { } #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn ollama_plugin_path() -> PathBuf { - let offline_app = "ollama_ai_plugin"; +pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { #[cfg(target_os = "windows")] - return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + { + // Use LOCALAPPDATA for a user-specific installation path on Windows. + let local_appdata = + std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Program Files".to_string()); + return std::path::PathBuf::from(local_appdata) + .join("Programs\\appflowy_plugin\\ollama_ai_plugin.exe"); + } #[cfg(target_os = "macos")] - return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + { + let offline_app = "ollama_ai_plugin"; + return std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + } #[cfg(target_os = "linux")] - return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + { + let offline_app = "ollama_ai_plugin"; + return std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + } } From 2e4beb065251b4cef1a213d11f2703bd510abfe5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 00:35:52 +0800 Subject: [PATCH 085/384] chore: enable windows --- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 15bde0e431..d0e7288df1 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -236,7 +236,7 @@ impl LocalAIResourceController { let llm_setting = self.get_llm_setting(); let bin_path = match get_operating_system() { - OperatingSystem::MacOS => { + OperatingSystem::MacOS | OperatingSystem::Windows => { let path = ollama_plugin_path(); if !path.exists() { return Err(FlowyError::new( From d29a90a472ac328ce794fac1ec41403b85edb339 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 08:54:32 +0800 Subject: [PATCH 086/384] chore: init ai plugin on separate thread --- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++++---------- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 6 +-- .../flowy-ai/src/local_ai/controller.rs | 34 +++++++------- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 7 ++- frontend/rust-lib/flowy-core/src/lib.rs | 1 + .../flowy-core/src/user_state_callback.rs | 27 ++++++++++- 6 files changed, 72 insertions(+), 49 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 7850d8767b..be2b2c8bee 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -80,8 +80,8 @@ impl AIManager { )); let cloned_local_ai = local_ai.clone(); - tokio::task::spawn_blocking(move || { - futures::executor::block_on(cloned_local_ai.init_plugin_when_first_run()); + tokio::spawn(async move { + cloned_local_ai.observe_plugin_resource().await; }); let external_service = Arc::new(query_service); @@ -103,7 +103,7 @@ impl AIManager { } pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { - let _ = self.local_ai.reload().await; + self.local_ai.reload().await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c44b62be01..493b744152 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -122,7 +122,7 @@ impl LocalAIController { } #[instrument(level = "debug", skip_all)] - pub async fn init_plugin_when_first_run(&self) { + pub async fn observe_plugin_resource(&self) { debug!( "[AI Plugin] init plugin when first run. thread: {:?}", std::thread::current().id() @@ -160,26 +160,15 @@ impl LocalAIController { } } }); - - // Perform initial initialization if required. - if self.should_init_plugin().await { - try_init_plugin(&self.resource, &self.ai_plugin).await; - } else { - trace!("[AI Plugin] local ai is able to start"); - } } pub async fn reload(&self) -> FlowyResult<()> { let is_enabled = self.is_enabled(); + self.toggle_plugin(is_enabled).await?; Ok(()) } - /// Returns true if the local AI is enabled and ready to use. - pub async fn should_init_plugin(&self) -> bool { - self.is_enabled() && self.resource.is_resource_ready().await - } - /// Indicate whether the local AI plugin is running. pub fn is_running(&self) -> bool { if !self.is_enabled() { @@ -255,17 +244,24 @@ impl LocalAIController { setting, std::thread::current().id() ); - self.destroy_chat_plugin().await?; self.resource.set_llm_setting(setting).await?; self.reload().await?; Ok(()) } + #[instrument(level = "debug", skip_all)] pub async fn get_local_ai_state(&self) -> LocalAIPB { + let start = std::time::Instant::now(); let enabled = self.is_enabled(); let is_app_downloaded = self.resource.is_app_downloaded(); let state = self.ai_plugin.get_plugin_running_state(); let lack_of_resource = self.resource.get_lack_of_resource().await; + let elapsed = start.elapsed(); + debug!( + "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", + elapsed, + std::thread::current().id() + ); LocalAIPB { enabled, is_app_downloaded, @@ -441,11 +437,15 @@ impl LocalAIController { #[instrument(level = "debug", skip_all, err)] async fn initialize_ai_plugin( - llm_chat: &Arc, + plugin: &Arc, llm_resource: &Arc, ret: Option>, ) -> FlowyResult<()> { - let llm_chat = llm_chat.clone(); + let plugin = plugin.clone(); + if plugin.get_plugin_running_state().is_loading() { + return Ok(()); + } + let lack_of_resource = llm_resource.get_lack_of_resource().await; chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, @@ -488,7 +488,7 @@ async fn initialize_ai_plugin( std::thread::current().id() ); - match llm_chat.init_chat_plugin(config).await { + match plugin.init_chat_plugin(config).await { Ok(_) => {}, Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index d18e045cbe..11063e43d6 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -87,19 +87,18 @@ pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { // Use LOCALAPPDATA for a user-specific installation path on Windows. let local_appdata = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Program Files".to_string()); - return std::path::PathBuf::from(local_appdata) - .join("Programs\\appflowy_plugin\\ollama_ai_plugin.exe"); + std::path::PathBuf::from(local_appdata).join("Programs\\appflowy_plugin\\ollama_ai_plugin.exe") } #[cfg(target_os = "macos")] { let offline_app = "ollama_ai_plugin"; - return std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) } #[cfg(target_os = "linux")] { let offline_app = "ollama_ai_plugin"; - return std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 4c84e73c08..2c41c1f205 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -255,6 +255,7 @@ impl AppFlowyCore { server_provider: server_provider.clone(), storage_manager: storage_manager.clone(), ai_manager: ai_manager.clone(), + runtime: runtime.clone(), }; let collab_interact_impl = CollabInteractImpl { diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index a43c28f90b..5a6a2e7b2a 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::Context; use client_api::entity::billing_dto::SubscriptionPlan; -use tracing::{event, info}; +use tracing::{error, event, info}; use collab_entity::CollabType; use collab_integrate::collab_builder::AppFlowyCollabBuilder; @@ -15,6 +15,7 @@ use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; +use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; use crate::server_layer::{Server, ServerProvider}; @@ -27,6 +28,20 @@ pub(crate) struct UserStatusCallbackImpl { pub(crate) server_provider: Arc, pub(crate) storage_manager: Arc, pub(crate) ai_manager: Arc, + // By default, all callback will run on the caller thread. If you don't want to block the caller + // thread, you can use runtime to spawn a new task. + pub(crate) runtime: Arc, +} + +impl UserStatusCallbackImpl { + fn init_ai_component(&self, workspace_id: String) { + let cloned_ai_manager = self.ai_manager.clone(); + self.runtime.spawn(async move { + if let Err(err) = cloned_ai_manager.initialize(&workspace_id).await { + error!("Failed to initialize AIManager: {:?}", err); + } + }); + } } #[async_trait] @@ -70,7 +85,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize(user_id, authenticator == &Authenticator::Local) .await?; self.document_manager.initialize(user_id).await?; - self.ai_manager.initialize(&user_workspace.id).await?; + + let workspace_id = user_workspace.id.clone(); + self.init_ai_component(workspace_id); Ok(()) } @@ -97,6 +114,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize(user_id, authenticator.is_local()) .await?; self.document_manager.initialize(user_id).await?; + + let workspace_id = user_workspace.id.clone(); + self.init_ai_component(workspace_id); Ok(()) } @@ -175,6 +195,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize_with_new_user(user_profile.uid) .await .context("DocumentManager error")?; + + let workspace_id = user_workspace.id.clone(); + self.init_ai_component(workspace_id); Ok(()) } From 8b2e769fca84e4f80e4740ffcb820ade4ea88e65 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 10:24:55 +0800 Subject: [PATCH 087/384] chore: ai setting ui --- .../settings/ai/local_ai_bloc.dart | 5 +- .../setting_ai_view/local_ai_setting.dart | 11 ++- .../local_ai_setting_panel.dart | 99 +++++++------------ frontend/resources/translations/en.json | 3 +- .../flowy-ai/src/local_ai/controller.rs | 57 +++++++---- 5 files changed, 82 insertions(+), 93 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 3c3d20039d..66ba7fe572 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -52,7 +52,8 @@ class LocalAIToggleBloc extends Bloc { (localAI) { emit( state.copyWith( - pageIndicator: LocalAIToggleStateIndicator.ready(localAI.enabled), + pageIndicator: + LocalAIToggleStateIndicator.isEnabled(localAI.enabled), ), ); }, @@ -88,6 +89,6 @@ class LocalAIToggleState with _$LocalAIToggleState { class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator { // when start downloading the model const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError; - const factory LocalAIToggleStateIndicator.ready(bool isEnabled) = _Ready; + const factory LocalAIToggleStateIndicator.isEnabled(bool isEnabled) = _Ready; const factory LocalAIToggleStateIndicator.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 706ecdd1a1..2a88008565 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -38,10 +38,11 @@ class _LocalAISettingState extends State { listener: (context, state) { final controller = ExpandableController.of(context, required: true)!; + state.pageIndicator.when( - error: (_) => controller.expanded = false, - ready: (enabled) => controller.expanded = enabled, - loading: () => controller.expanded = false, + error: (_) => controller.expanded = true, + isEnabled: (enabled) => controller.expanded = enabled, + loading: () => controller.expanded = true, ); }, child: ExpandablePanel( @@ -95,9 +96,9 @@ class LocalAISettingHeader extends StatelessWidget { return const SizedBox.shrink(); }, loading: () { - return const CircularProgressIndicator.adaptive(); + return const SizedBox.shrink(); }, - ready: (isEnabled) { + isEnabled: (isEnabled) { return Row( children: [ FlowyText( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart index 66379d347a..cdbba0f28a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -21,70 +21,41 @@ class LocalAISettingPanel extends StatelessWidget { ), ], child: ExpandableNotifier( - child: BlocListener( - listener: (context, state) { - // Listen to the toggle state and expand the panel if the state is ready. - final controller = ExpandableController.of( - context, - required: true, - )!; - - // Neet to wrap with WidgetsBinding.instance.addPostFrameCallback otherwise the - // ExpandablePanel not expanded sometimes. Maybe because the ExpandablePanel is not - // built yet when the listener is called. - WidgetsBinding.instance.addPostFrameCallback( - (_) { - state.pageIndicator.when( - error: (_) => controller.expanded = false, - ready: (enabled) { - controller.expanded = enabled; - context.read().add( - const LocalAISettingPanelEvent.started(), - ); + initialExpanded: true, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const SizedBox.shrink(), + collapsed: const SizedBox.shrink(), + expanded: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocBuilder( + builder: (context, state) { + // If the progress indicator is startLocalAIApp, then don't show the LLM model. + if (state.progressIndicator == + const LocalAIProgress.downloadLocalAIApp()) { + return const SizedBox.shrink(); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + OllamaSettingPage(), + VSpace(6), + _LocalAIStateWidget(), + ], + ); + } }, - loading: () => controller.expanded = false, - ); - }, - debugLabel: 'LocalAI.showLocalAIChatSetting', - ); - }, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: const SizedBox.shrink(), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - // child: _LocalLLMInfoWidget(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BlocBuilder( - builder: (context, state) { - // If the progress indicator is startLocalAIApp, then don't show the LLM model. - if (state.progressIndicator == - const LocalAIProgress.downloadLocalAIApp()) { - return const SizedBox.shrink(); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - OllamaSettingPage(), - VSpace(6), - _LocalAIStateWidget(), - ], - ); - } - }, - ), - ], - ), + ), + ], ), ), ), @@ -117,7 +88,7 @@ class _LocalAIStateWidget extends StatelessWidget { child: child, ); } else { - return const SizedBox.shrink(); + return const PluginStateIndicator(); } }, ); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e6a5658865..66625d124d 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -868,7 +868,8 @@ "openModelDirectory": "Open folder", "localAISetupInstruction1": "Follow the", "localAISetupInstruction2": "instruction", - "localAISetupInstruction3": "to setup your Ollama" + "localAISetupInstruction3": "to setup your Ollama", + "startLocalAI": "It may take a few seconds to start the local AI" } }, "planPage": { diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 493b744152..7fff47c89d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -44,7 +44,6 @@ impl Default for LocalAISetting { } } -const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1"; pub struct LocalAIController { @@ -87,27 +86,29 @@ impl LocalAIController { let mut running_state_rx = local_ai.subscribe_running_state(); let cloned_llm_res = local_ai_resource.clone(); let cloned_store_preferences = store_preferences.clone(); + let cloned_user_service = user_service.clone(); tokio::spawn(async move { while let Some(state) = running_state_rx.next().await { - info!("[AI Plugin] state: {:?}", state); - let ready = cloned_llm_res.is_app_downloaded(); - let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; + if let Ok(workspace_id) = cloned_user_service.workspace_id() { + let key = local_ai_enabled_key(&workspace_id); + info!("[AI Plugin] state: {:?}", state); + let ready = cloned_llm_res.is_app_downloaded(); + let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; - let new_state = RunningStatePB::from(state); - let enabled = cloned_store_preferences - .get_bool(APPFLOWY_LOCAL_AI_ENABLED) - .unwrap_or(true); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - is_app_downloaded: ready, - lack_of_resource, - state: new_state, - }) - .send(); + let new_state = RunningStatePB::from(state); + let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + is_app_downloaded: ready, + lack_of_resource, + state: new_state, + }) + .send(); + } } }); @@ -428,8 +429,21 @@ impl LocalAIController { error!("[AI Plugin] failed to initialize local ai: {:?}", err); } let _ = rx.await; - } else if let Err(err) = self.ai_plugin.destroy_chat_plugin().await { - error!("[AI Plugin] failed to destroy plugin: {:?}", err); + } else { + if let Err(err) = self.ai_plugin.destroy_chat_plugin().await { + error!("[AI Plugin] failed to destroy plugin: {:?}", err); + } + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + is_app_downloaded: true, + state: RunningStatePB::Stopped, + lack_of_resource: None, + }) + .send(); } Ok(()) } @@ -537,6 +551,7 @@ impl LLMResourceService for LLMResourceServiceImpl { } } +const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; fn local_ai_enabled_key(workspace_id: &str) -> String { format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) } From 4e0d9fdb0b0c861e4162f9609dde0eff9aec36f0 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:08:22 +0800 Subject: [PATCH 088/384] fix: don't include icon while exporting callout to md --- .../editor_plugins/parsers/callout_node_parser.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index 867fcf236f..92dbf776bf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -10,7 +10,6 @@ class CalloutNodeParser extends NodeParser { @override String transform(Node node, DocumentMarkdownEncoder? encoder) { assert(node.children.isEmpty); - final icon = node.attributes[CalloutBlockKeys.icon]; final delta = node.delta ?? Delta() ..insert(''); final String markdown = DeltaMarkdownEncoder() @@ -19,7 +18,6 @@ class CalloutNodeParser extends NodeParser { .map((e) => '> $e') .join('\n'); return ''' -> $icon $markdown '''; From e69a09d332ba3431da33753dfa70887907d1d233 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 10 Mar 2025 11:46:17 +0800 Subject: [PATCH 089/384] feat: support nested list in callout block and quote block (#7479) * feat: support nested list in callout block * chore: update pubspec.yml * feat: add new quote block * feat: support nested list in quote block * feat: refacotr quote block * feat: optimize quote block align * feat: support nested list in quote block * fix: icon and drag menu overlap * chore: update appflowy editor version * feat: support trailing action builder for plugin blocks * chore: update appflowy editor version --- .../document_data_pb_extension.dart | 1 - .../presentation/editor_configuration.dart | 43 ++- .../document/presentation/editor_page.dart | 5 +- .../actions/block_action_option_cubit.dart | 3 +- .../actions/drag_to_reorder/util.dart | 3 +- .../actions/option/option_actions.dart | 3 +- .../option/turn_into_option_action.dart | 3 +- .../ai/ai_writer_block_component.dart | 5 + .../callout/callout_block_component.dart | 62 +++- .../callout/callout_block_shortcuts.dart | 16 +- .../simple_column_block_component.dart | 1 + .../simple_columns_block_component.dart | 1 + .../database_view_block_component.dart | 5 + .../error/error_block_component_builder.dart | 6 + .../file/file_block_component.dart | 2 + .../custom_image_block_component.dart | 2 + .../multi_image_block_component.dart | 2 + .../math_equation_block_component.dart | 6 + .../migration/editor_migration.dart | 3 +- .../aa_menu/_block_items.dart | 3 +- .../add_block_menu_item_builder.dart | 3 +- .../numbered_list/numbered_list_icon.dart | 2 +- .../outline/outline_block_component.dart | 6 + .../custom_page_block_component.dart | 1 + .../presentation/editor_plugins/plugins.dart | 1 + .../quote/quote_block_component.dart | 304 ++++++++++++++++++ .../quote/quote_block_shortcuts.dart | 16 +- .../simple_table_block_component.dart | 6 + .../simple_table_cell_block_component.dart | 5 + .../simple_table_row_block_component.dart | 5 + .../sub_page/sub_page_block_component.dart | 2 + .../toggle/toggle_block_component.dart | 6 + frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../document/turn_into/turn_into_test.dart | 3 +- 35 files changed, 505 insertions(+), 36 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index 574ae34af8..38bf2bcd14 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -13,7 +13,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' NodeIterator, NodeExternalValues, HeadingBlockKeys, - QuoteBlockKeys, NumberedListBlockKeys, BulletedListBlockKeys, blockComponentDelta; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 7e3b5347da..653bebf902 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -6,7 +6,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mo import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; @@ -121,10 +122,18 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) { }, indentPadding: (node, textDirection) { double padding = 26.0; + // only add indent padding for the top level node to align the children if (UniversalPlatform.isMobile && node.path.length == 1) { padding += EditorStyleCustomizer.nodeHorizontalPadding; } + + // in the quote block, we reduce the indent padding for the first level block. + // So we have to add more padding for the second level to avoid the drag menu overlay the quote icon. + if (node.isInQuote && node.level == 2) { + padding += 24; + } + return textDirection == TextDirection.ltr ? EdgeInsets.only(left: padding) : EdgeInsets.only(right: padding); @@ -203,6 +212,16 @@ void _customBlockOptionActions( ), ); + builder.actionTrailingBuilder = (context, state) { + if (context.node.parent?.type == QuoteBlockKeys.type) { + return const SizedBox( + width: 24, + height: 24, + ); + } + return const SizedBox.shrink(); + }; + builder.actionBuilder = (context, state) { double top = builder.configuration.padding(context.node).top; final type = context.node.type; @@ -592,6 +611,7 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( node: node, configuration: configuration, ), + indentPadding: (node, _) => EdgeInsets.zero, ), ); } @@ -798,8 +818,14 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder( configuration: configuration, textSpan: textSpan, ), + indentPadding: (node, _) => EdgeInsets.only(left: 42), ), - inlinePadding: const EdgeInsets.symmetric(vertical: 8.0), + inlinePadding: (node) { + if (node.children.isEmpty) { + return const EdgeInsets.symmetric(vertical: 8.0); + } + return EdgeInsets.only(top: 8.0, bottom: 2.0); + }, defaultColor: calloutBGColor, ); } @@ -1065,3 +1091,16 @@ TextAlign _buildTextAlignInTableCell( return node.tableAlign.textAlign; } + +extension on Node { + bool get isInQuote { + Node? parent = this.parent; + while (parent != null) { + if (parent.type == QuoteBlockKeys.type) { + return true; + } + parent = parent.parent; + } + return false; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 2f2b2b0aa2..83e300146e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -154,7 +154,10 @@ class _AppFlowyEditorPageState extends State InlineMathEquationKeys.formula, ]); - indentableBlockTypes.add(ToggleListBlockKeys.type); + indentableBlockTypes.addAll([ + ToggleListBlockKeys.type, + CalloutBlockKeys.type, + ]); convertibleBlockTypes.addAll([ ToggleListBlockKeys.type, CalloutBlockKeys.type, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index 52b518a718..733da3fd71 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -10,7 +10,8 @@ import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockKeys, quoteNode; import 'package:flutter_bloc/flutter_bloc.dart'; class BlockActionOptionState {} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index 2e56331faf..da06bdc52b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -1,7 +1,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockKeys, quoteNode; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart index 14ec6773a6..571cb4baa0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockKeys, quoteNode; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart index ac3774a511..c69233e440 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart @@ -5,7 +5,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockKeys, quoteNode; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 81a52adb69..3835ff7fd4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -66,6 +66,10 @@ class AIWriterBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -83,6 +87,7 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 1d9f70254f..45a5a93ac8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -9,7 +9,6 @@ import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; import '../base/emoji_picker_button.dart'; @@ -80,7 +79,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { }); final Color defaultColor; - final EdgeInsets inlinePadding; + final EdgeInsets Function(Node node) inlinePadding; @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -96,12 +95,15 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @override - BlockComponentValidate get validate => - (node) => node.delta != null && node.children.isEmpty; + BlockComponentValidate get validate => (node) => node.delta != null; } // the main widget for rendering the callout block @@ -111,13 +113,14 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.defaultColor, required this.inlinePadding, }); final Color defaultColor; - final EdgeInsets inlinePadding; + final EdgeInsets Function(Node node) inlinePadding; @override State createState() => @@ -132,7 +135,8 @@ class _CalloutBlockComponentWidgetState BlockComponentConfigurable, BlockComponentTextDirectionMixin, BlockComponentAlignMixin, - BlockComponentBackgroundColorMixin { + BlockComponentBackgroundColorMixin, + NestedBlockComponentStatefulWidgetMixin { // the key used to forward focus to the richtext child @override final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); @@ -170,21 +174,45 @@ class _CalloutBlockComponentWidgetState try { result = EmojiIconData(FlowyIconType.values.byName(type), icon); } catch (e) { - Log.error( - 'get emoji error with icon:[$icon], type:[$type] within alloutBlockComponentWidget', + Log.info( + 'get emoji error with icon:[$icon], type:[$type] within calloutBlockComponentWidget', e, ); } return result; } - // get access to the editor state via provider @override - late final editorState = Provider.of(context, listen: false); + Widget buildComponentWithChildren(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + left: cachedLeft, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + color: backgroundColor, + ), + ), + ), + NestedListWidget( + indentPadding: indentPadding.copyWith(bottom: 8), + child: buildComponent(context, withBackgroundColor: false), + children: editorState.renderer.buildList( + context, + widget.node.children, + ), + ), + ], + ); + } // build the callout block widget @override - Widget build(BuildContext context) { + Widget buildComponent( + BuildContext context, { + bool withBackgroundColor = true, + }) { final textDirection = calculateTextDirection( layoutDirection: Directionality.maybeOf(context), ); @@ -192,10 +220,10 @@ class _CalloutBlockComponentWidgetState Widget child = Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), + borderRadius: const BorderRadius.all(Radius.circular(6.0)), color: backgroundColor, ), - padding: widget.inlinePadding, + padding: widget.inlinePadding(widget.node), width: double.infinity, alignment: alignment, child: Row( @@ -203,7 +231,7 @@ class _CalloutBlockComponentWidgetState mainAxisSize: MainAxisSize.min, textDirection: textDirection, children: [ - if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0), + if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), // the emoji picker button for the note EmojiPickerButton( // force to refresh the popover state @@ -222,7 +250,7 @@ class _CalloutBlockComponentWidgetState if (!r.keepOpen) controller?.close(); }, ), - if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0), + if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), Flexible( child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), @@ -236,7 +264,7 @@ class _CalloutBlockComponentWidgetState child = Padding( key: blockComponentKey, - padding: padding, + padding: EdgeInsets.zero, child: child, ); @@ -245,6 +273,7 @@ class _CalloutBlockComponentWidgetState delegate: this, listenable: editorState.selectionNotifier, blockColor: editorState.editorStyle.selectionColor, + selectionAboveBlock: true, supportTypes: const [ BlockSelectionType.block, ], @@ -255,6 +284,7 @@ class _CalloutBlockComponentWidgetState child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart index 3c50661071..482364bbb5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart @@ -33,8 +33,20 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { if (HardwareKeyboard.instance.isShiftPressed) { await editorState.insertNewLine(); - } else { - await editorState.insertTextAtCurrentSelection('\n'); + } else if (node.children.isEmpty) { + // insert a new paragraph within the callout block + final path = node.path.child(0); + final transaction = editorState.transaction; + transaction.insertNode( + path, + paragraphNode(), + ); + transaction.afterSelection = Selection.collapsed( + Position( + path: path, + ), + ); + await editorState.apply(transaction); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart index 611f7ec67f..eaaffa3479 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -51,6 +51,7 @@ class SimpleColumnBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index 43fd779d33..2761d8eabb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -67,6 +67,7 @@ class ColumnsBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart index 86df2ae172..87c2815091 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart @@ -42,6 +42,10 @@ class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -58,6 +62,7 @@ class DatabaseBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index a37ed29150..4394ff57c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -30,6 +30,10 @@ class ErrorBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -43,6 +47,7 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -81,6 +86,7 @@ class _ErrorBlockComponentWidgetState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart index 52fc2e717f..afd8188cac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart @@ -141,6 +141,7 @@ class FileBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -303,6 +304,7 @@ class FileBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index 0471358464..5d544f535a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -123,6 +123,7 @@ class CustomImageBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.showMenu = false, this.menuBuilder, @@ -235,6 +236,7 @@ class CustomImageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart index 88f60db494..51da975938 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart @@ -82,6 +82,7 @@ class MultiImageBlockComponent extends BlockComponentStatefulWidget { this.menuBuilder, super.configuration = const BlockComponentConfiguration(), super.actionBuilder, + super.actionTrailingBuilder, }); final bool showMenu; @@ -190,6 +191,7 @@ class MultiImageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index e9d6b3297e..2f724061ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -79,6 +79,10 @@ class MathEquationBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -94,6 +98,7 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -157,6 +162,7 @@ class MathEquationBlockComponentWidgetState child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart index be9063a6c8..41bb8ce873 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart @@ -7,7 +7,8 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; import 'package:string_validator/string_validator.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart index 57670afadd..8e1a8533e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart @@ -6,7 +6,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockKeys, quoteNode; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart index 4ae243575c..b3df4dfd39 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart @@ -12,7 +12,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart index 2f31a68a73..3fad80bf32 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart @@ -29,7 +29,7 @@ class NumberedListIcon extends StatelessWidget { ); return Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(left: 6.0, right: 10.0), child: Text( node.levelString, style: adjustedTextStyle, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index eaacb43ea6..023ff739ed 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -52,6 +52,10 @@ class OutlineBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -65,6 +69,7 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -102,6 +107,7 @@ class _OutlineBlockWidgetState extends State child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart index 5d7028f6e1..731ba4c7cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart @@ -25,6 +25,7 @@ class CustomPageBlockComponent extends BlockComponentStatelessWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.header, this.footer, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 8a475fd5a0..63aaf34e8d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -63,6 +63,7 @@ export 'outline/outline_block_component.dart'; export 'parsers/document_markdown_parsers.dart'; export 'parsers/markdown_parsers.dart'; export 'parsers/markdown_simple_table_parser.dart'; +export 'quote/quote_block_component.dart'; export 'quote/quote_block_shortcuts.dart'; export 'shortcuts/character_shortcuts.dart'; export 'shortcuts/command_shortcuts.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart new file mode 100644 index 0000000000..28729e64a6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart @@ -0,0 +1,304 @@ +import 'dart:async'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef QuoteBlockIconBuilder = Widget Function( + BuildContext context, + Node node, +); + +class QuoteBlockKeys { + const QuoteBlockKeys._(); + + static const String type = 'quote'; + + static const String delta = blockComponentDelta; + + static const String backgroundColor = blockComponentBackgroundColor; + + static const String textDirection = blockComponentTextDirection; +} + +Node quoteNode({ + Delta? delta, + String? textDirection, + Attributes? attributes, + Iterable? children, +}) { + attributes ??= {'delta': (delta ?? Delta()).toJson()}; + return Node( + type: QuoteBlockKeys.type, + attributes: { + ...attributes, + if (textDirection != null) QuoteBlockKeys.textDirection: textDirection, + }, + children: children ?? [], + ); +} + +class QuoteBlockComponentBuilder extends BlockComponentBuilder { + QuoteBlockComponentBuilder({ + super.configuration, + this.iconBuilder, + }); + + final QuoteBlockIconBuilder? iconBuilder; + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return QuoteBlockComponentWidget( + key: node.key, + node: node, + configuration: configuration, + iconBuilder: iconBuilder, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + BlockComponentValidate get validate => (node) => node.delta != null; +} + +class QuoteBlockComponentWidget extends BlockComponentStatefulWidget { + const QuoteBlockComponentWidget({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.actionTrailingBuilder, + super.configuration = const BlockComponentConfiguration(), + this.iconBuilder, + }); + + final QuoteBlockIconBuilder? iconBuilder; + + @override + State createState() => + _QuoteBlockComponentWidgetState(); +} + +class _QuoteBlockComponentWidgetState extends State + with + SelectableMixin, + DefaultSelectableMixin, + BlockComponentConfigurable, + BlockComponentBackgroundColorMixin, + BlockComponentTextDirectionMixin, + BlockComponentAlignMixin, + NestedBlockComponentStatefulWidgetMixin { + @override + final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); + + @override + GlobalKey> get containerKey => widget.node.key; + + @override + GlobalKey> blockComponentKey = GlobalKey( + debugLabel: QuoteBlockKeys.type, + ); + + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + ValueNotifier quoteBlockHeightNotifier = ValueNotifier(0); + + StreamSubscription? _transactionSubscription; + + final GlobalKey layoutBuilderKey = GlobalKey(); + + @override + void initState() { + super.initState(); + + _observerQuoteBlockChanges(); + } + + @override + void dispose() { + _transactionSubscription?.cancel(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + _updateQuoteBlockHeight(); + + return KeyedSubtree( + key: layoutBuilderKey, + child: node.children.isEmpty + ? buildComponent(context) + : buildComponentWithChildren(context), + ); + }, + ); + } + + @override + Widget buildComponent( + BuildContext context, { + bool withBackgroundColor = true, + }) { + final textDirection = calculateTextDirection( + layoutDirection: Directionality.maybeOf(context), + ); + + Widget child = AppFlowyRichText( + key: forwardKey, + delegate: this, + node: widget.node, + editorState: editorState, + textAlign: alignment?.toTextAlign ?? textAlign, + placeholderText: placeholderText, + textSpanDecorator: (textSpan) => textSpan.updateTextStyle( + textStyleWithTextSpan(textSpan: textSpan), + ), + placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( + placeholderTextStyleWithTextSpan(textSpan: textSpan), + ), + textDirection: textDirection, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, + cursorWidth: editorState.editorStyle.cursorWidth, + ); + + child = Container( + width: double.infinity, + alignment: alignment, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + textDirection: textDirection, + children: [ + widget.iconBuilder != null + ? widget.iconBuilder!(context, node) + : ValueListenableBuilder( + valueListenable: quoteBlockHeightNotifier, + builder: (context, height, child) { + return QuoteIcon(height: height); + }, + ), + Flexible( + child: child, + ), + ], + ), + ), + ); + + child = Container( + color: backgroundColor, + child: Padding( + key: blockComponentKey, + padding: padding, + child: child, + ), + ); + + child = BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + remoteSelection: editorState.remoteSelections, + blockColor: editorState.editorStyle.selectionColor, + supportTypes: const [ + BlockSelectionType.block, + ], + child: child, + ); + + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, + child: child, + ); + } + + return child; + } + + void _updateQuoteBlockHeight() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final renderObject = layoutBuilderKey.currentContext?.findRenderObject(); + if (renderObject != null && renderObject is RenderBox) { + quoteBlockHeightNotifier.value = + renderObject.size.height - padding.top * 2; + } else { + quoteBlockHeightNotifier.value = 0; + } + }); + } + + void _observerQuoteBlockChanges() { + _transactionSubscription = editorState.transactionStream.listen((event) { + final time = event.$1; + + if (time != TransactionTime.before) { + return; + } + + final transaction = event.$2; + final operations = transaction.operations; + for (final operation in operations) { + if (node.path.isAncestorOf(operation.path)) { + _updateQuoteBlockHeight(); + } + } + }); + } +} + +class QuoteIcon extends StatelessWidget { + const QuoteIcon({ + super.key, + this.height = 0, + }); + + final double height; + + @override + Widget build(BuildContext context) { + final textScaleFactor = + context.read().editorStyle.textScaleFactor; + return Container( + alignment: Alignment.center, + constraints: + const BoxConstraints(minWidth: 22, minHeight: 22, maxHeight: 22) * + textScaleFactor, + padding: const EdgeInsets.only(right: 6.0), + child: SizedBox( + width: 3 * textScaleFactor, + + // use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote + child: OverflowBox( + alignment: Alignment.topCenter, + maxHeight: height, + child: Container( + width: 3 * textScaleFactor, + height: height, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart index ba3ad6e7df..835d7bb47a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart @@ -31,8 +31,20 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { if (HardwareKeyboard.instance.isShiftPressed) { await editorState.insertNewLine(); - } else { - await editorState.insertTextAtCurrentSelection('\n'); + } else if (node.children.isEmpty) { + // insert a new paragraph within the callout block + final path = node.path.child(0); + final transaction = editorState.transaction; + transaction.insertNode( + path, + paragraphNode(), + ); + transaction.afterSelection = Selection.collapsed( + Position( + path: path, + ), + ); + await editorState.apply(transaction); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart index 5d4c3cab52..ee6020793c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart @@ -161,6 +161,10 @@ class SimpleTableBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -174,6 +178,7 @@ class SimpleTableBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); @@ -262,6 +267,7 @@ class _SimpleTableBlockWidgetState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart index 112343c6d8..29b3c3455f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart @@ -47,6 +47,10 @@ class SimpleTableCellBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -60,6 +64,7 @@ class SimpleTableCellBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart index d61d371e1b..99f23d1ee9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart @@ -39,6 +39,10 @@ class SimpleTableRowBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -52,6 +56,7 @@ class SimpleTableRowBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart index ad69d4c784..0ce2b74a74 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart @@ -73,6 +73,7 @@ class SubPageBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -295,6 +296,7 @@ class SubPageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 9fc0a02cec..9e93f80ce4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -111,6 +111,10 @@ class ToggleListBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), + actionTrailingBuilder: (context, state) => actionTrailingBuilder( + blockComponentContext, + state, + ), ); } @@ -124,6 +128,7 @@ class ToggleListBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, + super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.padding = const EdgeInsets.all(0), this.textStyleBuilder, @@ -247,6 +252,7 @@ class _ToggleListBlockComponentWidgetState child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, + actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index fc04deae5b..c29671f625 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: e2c9713 - resolved-ref: e2c9713104f00658c83ac7d1657e7a0fade171ad + ref: "63e92c1" + resolved-ref: "63e92c19f506dab795d6e1829f82e63a073e7f11" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f70f88ede7..29cf86decf 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "e2c9713" + ref: "63e92c1" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart index 387375c286..d4b0bfa45c 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart @@ -1,7 +1,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide quoteNode, QuoteBlockKeys; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; From c1612fe2983971829683d7ac645e8d8debfcf8c8 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 10 Mar 2025 11:46:55 +0800 Subject: [PATCH 090/384] feat: add custom icons for callout (#7449) --- .../editor_plugins/base/emoji_picker_button.dart | 2 +- .../callout/callout_block_component.dart | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index e25710c137..93b45cf46a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -208,7 +208,7 @@ class _MobileEmojiPickerButton extends StatelessWidget { MobileEmojiPickerScreen.iconSelectedType: emoji.type.name, MobileEmojiPickerScreen.uploadDocumentId: documentId, MobileEmojiPickerScreen.selectTabs: - tabs.map((e) => e.name).toList(), + tabs.map((e) => e.name).toList().join('-'), }, ).toString(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 45a5a93ac8..7865d8e13c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' @@ -217,7 +219,7 @@ class _CalloutBlockComponentWidgetState layoutDirection: Directionality.maybeOf(context), ); final (emojiSize, emojiButtonSize) = calculateEmojiSize(); - + final documentId = context.read()?.documentId; Widget child = Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(6.0)), @@ -227,7 +229,6 @@ class _CalloutBlockComponentWidgetState width: double.infinity, alignment: alignment, child: Row( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, textDirection: textDirection, children: [ @@ -245,6 +246,12 @@ class _CalloutBlockComponentWidgetState emojiSize: emojiSize, showBorder: false, buttonSize: emojiButtonSize, + documentId: documentId, + tabs: const [ + PickerTabType.emoji, + PickerTabType.icon, + PickerTabType.custom, + ], onSubmitted: (r, controller) { setEmojiIconData(r.data); if (!r.keepOpen) controller?.close(); From 41b99209f13b90bfb24a15ce90c9c323be9abc1a Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:10:59 +0800 Subject: [PATCH 091/384] fix: retain if is emoji --- .../editor_plugins/parsers/callout_node_parser.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index 92dbf776bf..45d8be9844 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; class CalloutNodeParser extends NodeParser { @@ -17,8 +18,15 @@ class CalloutNodeParser extends NodeParser { .split('\n') .map((e) => '> $e') .join('\n'); + final type = node.attributes[CalloutBlockKeys.iconType]; + final icon = type == FlowyIconType.emoji.name + ? node.attributes[CalloutBlockKeys.icon] + : null; + + final content = icon == null ? markdown : "> $icon\n$markdown"; + return ''' -$markdown +$content '''; } From 7b32a92290be25c6ef5d2c1922056e8175ca2c91 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 10 Mar 2025 13:04:29 +0800 Subject: [PATCH 092/384] fix: callout block build error (#7491) --- .../editor_plugins/callout/callout_block_component.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 7865d8e13c..9aa97a20b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -11,6 +11,7 @@ import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; import '../base/emoji_picker_button.dart'; From 0cefaf633c489ea8785381cfa6c5761c979d30bf Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:56:01 +0800 Subject: [PATCH 093/384] fix: fix test --- .../editor_plugins/parsers/callout_node_parser.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index 45d8be9844..b16a44cf6f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -19,7 +19,7 @@ class CalloutNodeParser extends NodeParser { .map((e) => '> $e') .join('\n'); final type = node.attributes[CalloutBlockKeys.iconType]; - final icon = type == FlowyIconType.emoji.name + final icon = type == FlowyIconType.emoji.name || type == null || type == "" ? node.attributes[CalloutBlockKeys.icon] : null; From a8b55ca3f07d79e513058ff615a4c5e884577f08 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 15:56:09 +0800 Subject: [PATCH 094/384] chore: update prompt --- .../lib/ai/service/ai_prompt_input_bloc.dart | 10 +++ .../settings/ai/plugin_state_bloc.dart | 88 ++++++++----------- .../local_ai_setting_panel.dart | 20 +---- .../pages/setting_ai_view/ollma_setting.dart | 6 +- .../pages/setting_ai_view/plugin_state.dart | 25 ++++-- frontend/resources/translations/en.json | 6 +- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-ai/src/local_ai/controller.rs | 2 +- .../flowy-ai/src/local_ai/resource.rs | 16 ++-- 10 files changed, 84 insertions(+), 97 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 2dff6ccbd2..2cad319cdd 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -37,6 +37,16 @@ class AIPromptInputBloc extends Bloc { (event, emit) { event.when( updateAIState: (LocalAIPB localAIState) { + if (localAIState.hasLackOfResource()) { + emit( + state.copyWith( + aiType: AIType.appflowyAI, + supportChatWithFile: false, + localAIState: localAIState, + ), + ); + return; + } // Only user enable chat with file and the plugin is already running final supportChatWithFile = localAIState.enabled && localAIState.state == RunningStatePB.Running; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart index 93b0417e63..f9727dfc89 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -6,7 +6,6 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl; part 'plugin_state_bloc.freezed.dart'; @@ -60,48 +59,46 @@ class PluginStateBloc extends Bloc { }, updateLocalAIState: (LocalAIPB aiState) { // if the offline ai is not started, ask user to start it - if (aiState.isAppDownloaded) { - if (aiState.hasLackOfResource()) { - emit( - PluginStateState( - action: - PluginStateAction.lackOfResource(aiState.lackOfResource), - ), - ); - return; - } - - // Chech state of the plugin - switch (aiState.state) { - case RunningStatePB.ReadyToRun: - emit( - const PluginStateState( - action: PluginStateAction.readToRun(), - ), - ); - - case RunningStatePB.Connecting: - emit( - const PluginStateState( - action: PluginStateAction.initializingPlugin(), - ), - ); - case RunningStatePB.Running: - emit(const PluginStateState(action: PluginStateAction.running())); - break; - case RunningStatePB.Stopped: - emit( - state.copyWith(action: const PluginStateAction.restartPlugin()), - ); - default: - break; - } - } else { + if (aiState.hasLackOfResource()) { emit( - const PluginStateState( - action: PluginStateAction.downloadLocalAIApp(), + PluginStateState( + action: PluginStateAction.lackOfResource(aiState.lackOfResource), ), ); + return; + } + + // Chech state of the plugin + switch (aiState.state) { + case RunningStatePB.ReadyToRun: + emit( + const PluginStateState( + action: PluginStateAction.readToRun(), + ), + ); + + case RunningStatePB.Connecting: + emit( + const PluginStateState( + action: PluginStateAction.initializingPlugin(), + ), + ); + case RunningStatePB.Connected: + emit( + const PluginStateState( + action: PluginStateAction.initializingPlugin(), + ), + ); + break; + case RunningStatePB.Running: + emit(const PluginStateState(action: PluginStateAction.running())); + break; + case RunningStatePB.Stopped: + emit( + state.copyWith(action: const PluginStateAction.restartPlugin()), + ); + default: + break; } }, restartLocalAI: () async { @@ -110,15 +107,6 @@ class PluginStateBloc extends Bloc { ); unawaited(AIEventRestartLocalAI().send()); }, - downloadOfflineAIApp: () async { - final result = await AIEventGetLocalAIDownloadLink().send(); - await result.fold( - (app) async { - await launchUrl(Uri.parse(app.link)); - }, - (err) {}, - ); - }, resourceStateChange: (data) { emit( PluginStateState( @@ -136,7 +124,6 @@ class PluginStateEvent with _$PluginStateEvent { const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) = _UpdateLocalAIState; const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; - const factory PluginStateEvent.downloadOfflineAIApp() = _DownloadOfflineAIApp; const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) = _ResourceStateChange; } @@ -155,6 +142,5 @@ class PluginStateAction with _$PluginStateAction { const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; const factory PluginStateAction.running() = _PluginRunning; const factory PluginStateAction.restartPlugin() = _RestartPlugin; - const factory PluginStateAction.downloadLocalAIApp() = _DownloadLocalAIApp; const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart index cdbba0f28a..6b95f2e9d2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -71,25 +71,7 @@ class _LocalAIStateWidget extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (state.progressIndicator != null) { - final child = state.progressIndicator!.when( - downloadLocalAIApp: () => OpenOrDownloadOfflineAIApp( - onRetry: () { - context - .read() - .add(const LocalAISettingPanelEvent.started()); - }, - ), - checkPluginState: () => const PluginStateIndicator(), - ); - - return Padding( - padding: const EdgeInsets.only(top: 8), - child: child, - ); - } else { - return const PluginStateIndicator(); - } + return const PluginStateIndicator(); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart index 45394a9db5..04bd27372a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart @@ -28,8 +28,6 @@ class OllamaSettingPage extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _InstallOllamaInstruction(), - const VSpace(12), ListView.separated( shrinkWrap: true, itemCount: state.inputItems.length, @@ -40,6 +38,10 @@ class OllamaSettingPage extends StatelessWidget { }, ), const VSpace(6), + Opacity( + opacity: 0.6, + child: _InstallOllamaInstruction(), + ), _SaveButton(isEdited: state.isEdited), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index f1896b145b..73fd0f2f42 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -29,13 +29,6 @@ class PluginStateIndicator extends StatelessWidget { initializingPlugin: () => const InitLocalAIIndicator(), running: () => const _LocalAIRunning(), restartPlugin: () => const _RestartPluginButton(), - downloadLocalAIApp: () => OpenOrDownloadOfflineAIApp( - onRetry: () { - context - .read() - .add(const PluginStateEvent.started()); - }, - ), lackOfResource: (desc) => _LackOfResource(desc: desc), ); }, @@ -216,6 +209,22 @@ class _LackOfResource extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyText(desc); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + FlowySvgs.toast_warning_filled_s, + size: const Size.square(20.0), + blendMode: null, + ), + const HSpace(6), + Expanded( + child: FlowyText( + desc, + maxLines: 3, + ), + ) + ], + ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 66625d124d..de94737ba9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -866,9 +866,9 @@ "activeOfflineAI": "Active", "downloadOfflineAI": "Download", "openModelDirectory": "Open folder", - "localAISetupInstruction1": "Follow the", - "localAISetupInstruction2": "instruction", - "localAISetupInstruction3": "to setup your Ollama", + "localAISetupInstruction1": "Follow these", + "localAISetupInstruction2": "instructions", + "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI. Skip if you've already set it up", "startLocalAI": "It may take a few seconds to start the local AI" } }, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ddd730c1c3..be89f620d6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=99447e23d5563328e5a2c2b0749e44540359a818#99447e23d5563328e5a2c2b0749e44540359a818" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ab121037b42c9b8f3e874598eb4ee72b29ef46b4#ab121037b42c9b8f3e874598eb4ee72b29ef46b4" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=99447e23d5563328e5a2c2b0749e44540359a818#99447e23d5563328e5a2c2b0749e44540359a818" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ab121037b42c9b8f3e874598eb4ee72b29ef46b4#ab121037b42c9b8f3e874598eb4ee72b29ef46b4" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c73ac79d72..0c70b37e09 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "99447e23d5563328e5a2c2b0749e44540359a818" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "99447e23d5563328e5a2c2b0749e44540359a818" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ab121037b42c9b8f3e874598eb4ee72b29ef46b4" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ab121037b42c9b8f3e874598eb4ee72b29ef46b4" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 7fff47c89d..4b52851534 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -502,7 +502,7 @@ async fn initialize_ai_plugin( std::thread::current().id() ); - match plugin.init_chat_plugin(config).await { + match plugin.init_plugin(config).await { Ok(_) => {}, Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index d0e7288df1..1825ebea50 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -38,18 +38,16 @@ pub enum WatchDiskEvent { pub enum PendingResource { LocalAIAppNotDownloaded, - OllamaServerNotFound, - OllamaNotInstalled, + OllamaServerNotReady, MissingModel(String), } impl PendingResource { pub fn desc(self) -> String { match self { - PendingResource::LocalAIAppNotDownloaded => "Local AI app not downloaded".to_string(), - PendingResource::OllamaServerNotFound => "Ollama server not ready".to_string(), - PendingResource::OllamaNotInstalled => "Ollama not installed".to_string(), - PendingResource::MissingModel(model) => format!("Missing model: {}", model), + PendingResource::LocalAIAppNotDownloaded => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), + PendingResource::OllamaServerNotReady => "Ollama is not ready. Please follow the instructions to install Ollama".to_string(), + PendingResource::MissingModel(model) => format!("Cannot find the model: {}. Please use the ollama pull command to install the model", model), } } } @@ -184,7 +182,7 @@ impl LocalAIResourceController { "[LLM Resource] Ollama server is not responding at {}", setting.ollama_server_url ); - resources.push(PendingResource::OllamaServerNotFound); + resources.push(PendingResource::OllamaServerNotReady); return Ok(resources); }, } @@ -217,11 +215,11 @@ impl LocalAIResourceController { "[LLM Resource] 'ollama list' command failed with status: {:?}", output.status ); - resources.push(PendingResource::OllamaNotInstalled); + resources.push(PendingResource::OllamaServerNotReady); }, Err(e) => { error!("[LLM Resource] failed to execute 'ollama list': {:?}", e); - resources.push(PendingResource::OllamaNotInstalled); + resources.push(PendingResource::OllamaServerNotReady); }, } From c81f87dcdca2221024a980823d29a74419b9dd4c Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 10 Mar 2025 18:13:15 +0800 Subject: [PATCH 095/384] feat: make the columns block same width width the editor (#7493) * feat: make the columns block same width width the editor * chore: turn off column debug mode * feat: add block selection container in outline block * feat: use ratio instead of width in simple columns * fix: document rules * fix: turn off debug mode * fix: update the existing columns block data --- .../document/application/document_rules.dart | 22 ++++++ .../draggable_option_button.dart | 4 +- .../actions/drag_to_reorder/util.dart | 64 ++++++++--------- .../simple_column_block_component.dart | 53 ++++++++++---- .../simple_column_block_width_resizer.dart | 64 +++++++++++++---- .../columns/simple_column_node_extension.dart | 8 +-- .../simple_columns_block_component.dart | 70 ++++++++++++------- .../custom_image_block_component.dart | 3 +- .../outline/outline_block_component.dart | 29 +++++++- .../slash_menu_items/simple_columns_item.dart | 13 ++-- 10 files changed, 223 insertions(+), 107 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart index 230a5b8fa7..f530b1ef8d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart @@ -104,6 +104,28 @@ class DocumentRules { } else { // otherwise, delete the column deleteColumnsTransaction.deleteNode(column); + + final deletedColumnRatio = + column.attributes[SimpleColumnBlockKeys.ratio]; + if (deletedColumnRatio != null) { + // update the ratio of the columns + final columnsNode = column.columnsParent; + if (columnsNode != null) { + final length = columnsNode.children.length; + for (final columnNode in columnsNode.children) { + final ratio = + columnNode.attributes[SimpleColumnBlockKeys.ratio] ?? + 1.0 / length; + if (ratio != null) { + deleteColumnsTransaction.updateNode(columnNode, { + ...columnNode.attributes, + SimpleColumnBlockKeys.ratio: + ratio + deletedColumnRatio / (length - 1), + }); + } + } + } + } } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart index f09781591f..7bc1fba8d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart @@ -89,7 +89,7 @@ class _DraggableOptionButtonState extends State { interceptor: (context, targetNode) { // if the cursor node is in a columns block or a column block, // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.parentColumn; + final parentColumnNode = targetNode.columnParent; if (parentColumnNode != null) { final position = getDragAreaPosition( context, @@ -147,7 +147,7 @@ class _DraggableOptionButtonState extends State { interceptor: (context, targetNode) { // if the cursor node is in a columns block or a column block, // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.parentColumn; + final parentColumnNode = targetNode.columnParent; if (parentColumnNode != null) { final position = getDragAreaPosition( context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index da06bdc52b..c816934976 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' @@ -60,38 +59,37 @@ Future dragToMoveNode( // 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent // 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node final transaction = editorState.transaction; - final targetNodeParent = targetNode.parentColumnsBlock; + final targetNodeParent = targetNode.columnsParent; + if (targetNodeParent != null) { final length = targetNodeParent.children.length; + final ratios = targetNodeParent.children + .map( + (e) => + e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? + 1.0 / length, + ) + .map((e) => e * length / (length + 1)) + .toList(); + final columnNode = simpleColumnNode( children: [node.deepCopy()], - width: (node.rect.width * 1 / (length + 1)).clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + ratio: 1.0 / (length + 1), ); - - for (final column in targetNodeParent.children) { - final width = - column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? - SimpleColumnsBlockConstants.minimumColumnWidth; + for (final (index, column) in targetNodeParent.children.indexed) { transaction.updateNode(column, { ...column.attributes, - SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + SimpleColumnBlockKeys.ratio: ratios[index], }); } transaction.insertNode(targetNode.path.next, columnNode); transaction.deleteNode(node); } else { - final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [targetNode.deepCopy()], width: width), - simpleColumnNode(children: [node.deepCopy()], width: width), + simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), + simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), ], ); @@ -109,39 +107,37 @@ Future dragToMoveNode( // 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent // 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node final transaction = editorState.transaction; - final targetNodeParent = targetNode.parentColumnsBlock; + final targetNodeParent = targetNode.columnsParent; if (targetNodeParent != null) { // find the previous sibling node of the target node final length = targetNodeParent.children.length; + final ratios = targetNodeParent.children + .map( + (e) => + e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? + 1.0 / length, + ) + .map((e) => e * length / (length + 1)) + .toList(); final columnNode = simpleColumnNode( children: [node.deepCopy()], - width: (node.rect.width * 1 / (length + 1)).clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + ratio: 1.0 / (length + 1), ); - for (final column in targetNodeParent.children) { - final width = - column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? - SimpleColumnsBlockConstants.minimumColumnWidth; + for (final (index, column) in targetNodeParent.children.indexed) { transaction.updateNode(column, { ...column.attributes, - SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + SimpleColumnBlockKeys.ratio: ratios[index], }); } transaction.insertNode(targetNode.path.previous, columnNode); transaction.deleteNode(node); } else { - final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [node.deepCopy()], width: width), - simpleColumnNode(children: [targetNode.deepCopy()], width: width), + simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), + simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart index eaaffa3479..a7259d54b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -7,27 +7,62 @@ import 'package:provider/provider.dart'; Node simpleColumnNode({ List? children, - double? width, + double? ratio, }) { return Node( type: SimpleColumnBlockKeys.type, children: children ?? [paragraphNode()], attributes: { - SimpleColumnBlockKeys.width: width, + SimpleColumnBlockKeys.ratio: ratio, }, ); } +extension SimpleColumnBlockAttributes on Node { + // get the next column node of the current column node + // if the current column node is the last column node, return null + Node? get nextColumn { + final index = path.last; + final parent = this.parent; + if (parent == null || index == parent.children.length - 1) { + return null; + } + return parent.children[index + 1]; + } + + // get the previous column node of the current column node + // if the current column node is the first column node, return null + Node? get previousColumn { + final index = path.last; + final parent = this.parent; + if (parent == null || index == 0) { + return null; + } + return parent.children[index - 1]; + } +} + class SimpleColumnBlockKeys { const SimpleColumnBlockKeys._(); static const String type = 'simple_column'; + /// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead. + /// + /// This field is no longer used since v0.6.9 + @Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.') static const String width = 'width'; + + /// The ratio of the column width. + /// + /// The value is a double number between 0 and 1. + static const String ratio = 'ratio'; } class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder { - SimpleColumnBlockComponentBuilder({super.configuration}); + SimpleColumnBlockComponentBuilder({ + super.configuration, + }); @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -74,16 +109,6 @@ class SimpleColumnBlockComponentState extends State late final EditorState editorState = context.read(); - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { Widget child = Column( @@ -121,7 +146,7 @@ class SimpleColumnBlockComponentState extends State if (SimpleColumnsBlockConstants.enableDebugBorder) { child = Container( color: Colors.green.withValues( - alpha: 0.2, + alpha: 0.3, ), child: child, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart index dad0dea132..f94056cd3a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart @@ -1,6 +1,5 @@ import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -26,6 +25,13 @@ class _SimpleColumnBlockWidthResizerState ValueNotifier isHovering = ValueNotifier(false); + @override + void dispose() { + isHovering.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return MouseRegion( @@ -78,25 +84,47 @@ class _SimpleColumnBlockWidthResizerState // update the column width in memory final columnNode = widget.columnNode; + final columnsNode = columnNode.columnsParent; + if (columnsNode == null) { + return; + } + final editorWidth = columnsNode.rect.width; final rect = columnNode.rect; - final width = - columnNode.attributes[SimpleColumnBlockKeys.width] ?? rect.width; + final width = rect.width; + final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio]; final newWidth = width + details.delta.dx; + final transaction = widget.editorState.transaction; + final newRatio = newWidth / editorWidth; transaction.updateNode(columnNode, { ...columnNode.attributes, - SimpleColumnBlockKeys.width: newWidth.clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + SimpleColumnBlockKeys.ratio: newRatio, }); - final columnsNode = columnNode.parent; - if (columnsNode != null) { - transaction.updateNode(columnsNode, { - ...columnsNode.attributes, - ColumnsBlockKeys.columnCount: columnsNode.children.length, + + if (newRatio < 0.1 && newRatio < originalRatio) { + return; + } + + final nextColumn = columnNode.nextColumn; + if (nextColumn != null) { + final nextColumnRect = nextColumn.rect; + final nextColumnWidth = nextColumnRect.width; + final newNextColumnWidth = nextColumnWidth - details.delta.dx; + final newNextColumnRatio = newNextColumnWidth / editorWidth; + if (newNextColumnRatio < 0.1) { + return; + } + transaction.updateNode(nextColumn, { + ...nextColumn.attributes, + SimpleColumnBlockKeys.ratio: newNextColumnRatio, }); } + + transaction.updateNode(columnsNode, { + ...columnsNode.attributes, + ColumnsBlockKeys.columnCount: columnsNode.children.length, + }); + widget.editorState.apply( transaction, options: ApplyOptions(inMemoryUpdate: true), @@ -113,9 +141,15 @@ class _SimpleColumnBlockWidthResizerState // apply the transaction again to make sure the width is updated final transaction = widget.editorState.transaction; - transaction.updateNode(widget.columnNode, { - ...widget.columnNode.attributes, - }); + final columnsNode = widget.columnNode.columnsParent; + if (columnsNode == null) { + return; + } + for (final columnNode in columnsNode.children) { + transaction.updateNode(columnNode, { + ...columnNode.attributes, + }); + } widget.editorState.apply(transaction); isDragging = false; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart index 3f4f73fd49..05389fb760 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart @@ -3,7 +3,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; extension SimpleColumnNodeExtension on Node { /// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock]. - Node? get parentColumnsBlock { + Node? get columnsParent { Node? currentNode = parent; while (currentNode != null) { if (currentNode.type == SimpleColumnsBlockKeys.type) { @@ -15,7 +15,7 @@ extension SimpleColumnNodeExtension on Node { } /// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock]. - Node? get parentColumn { + Node? get columnParent { Node? currentNode = parent; while (currentNode != null) { if (currentNode.type == SimpleColumnBlockKeys.type) { @@ -27,8 +27,8 @@ extension SimpleColumnNodeExtension on Node { } /// Returns whether the current node is in a [SimpleColumnsBlock]. - bool get isInColumnsBlock => parentColumnsBlock != null; + bool get isInColumnsBlock => columnsParent != null; /// Returns whether the current node is in a [SimpleColumnBlock]. - bool get isInColumnBlock => parentColumn != null; + bool get isInColumnBlock => columnParent != null; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index 2761d8eabb..f5b0b14400 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; @@ -5,20 +7,19 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; // if the children is not provided, it will create two columns by default. // if the columnCount is provided, it will create the specified number of columns. Node simpleColumnsNode({ List? children, int? columnCount, - double? width, + double? ratio, }) { columnCount ??= 2; children ??= List.generate( columnCount, (index) => simpleColumnNode( - width: width, + ratio: ratio, children: [paragraphNode()], ), ); @@ -91,6 +92,13 @@ class ColumnsBlockComponentState extends State final ScrollController scrollController = ScrollController(); + @override + void initState() { + super.initState(); + + _updateColumnsBlock(); + } + @override void dispose() { scrollController.dispose(); @@ -100,23 +108,12 @@ class ColumnsBlockComponentState extends State @override Widget build(BuildContext context) { - Widget child = SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: scrollController, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildChildren(), - ), + Widget child = Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildChildren(), ); - if (UniversalPlatform.isDesktop) { - // only show the scrollbar on desktop - child = Scrollbar( - controller: scrollController, - child: child, - ); - } - child = Align( alignment: Alignment.topLeft, child: IntrinsicHeight( @@ -148,19 +145,18 @@ class ColumnsBlockComponentState extends State } List _buildChildren() { + final length = node.children.length; final children = []; - for (var i = 0; i < node.children.length; i++) { + for (var i = 0; i < length; i++) { final childNode = node.children[i]; - final width = - childNode.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? - SimpleColumnsBlockConstants.minimumColumnWidth; + final double ratio = + childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? + 1.0 / length; + Widget child = editorState.renderer.build(context, childNode); - child = SizedBox( - width: width.clamp( - SimpleColumnsBlockConstants.minimumColumnWidth, - double.infinity, - ), + child = Expanded( + flex: (max(ratio, 0.1) * 10000).toInt(), child: child, ); @@ -176,6 +172,26 @@ class ColumnsBlockComponentState extends State return children; } + // Update the existing columns block data + // if the column ratio is not existing, it will be set to 1.0 / columnCount + void _updateColumnsBlock() { + final transaction = editorState.transaction; + final length = node.children.length; + for (int i = 0; i < length; i++) { + final childNode = node.children[i]; + final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio]; + if (ratio == null) { + transaction.updateNode(childNode, { + ...childNode.attributes, + SimpleColumnBlockKeys.ratio: 1.0 / length, + }); + } + } + if (transaction.operations.isNotEmpty) { + editorState.apply(transaction); + } + } + @override Position start() => Position(path: widget.node.path); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index 5d544f535a..bbb9dc2abc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -227,6 +227,7 @@ class CustomImageBlockComponentState extends State delegate: this, listenable: editorState.selectionNotifier, blockColor: editorState.editorStyle.selectionColor, + selectionAboveBlock: true, supportTypes: const [BlockSelectionType.block], child: child, ); @@ -313,7 +314,7 @@ class CustomImageBlockComponentState extends State }) { final imageBox = imageKey.currentContext?.findRenderObject(); if (imageBox is RenderBox) { - return Offset.zero & imageBox.size; + return padding.topLeft & imageBox.size; } return Rect.zero; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index 023ff739ed..e3120356d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -81,7 +81,9 @@ class _OutlineBlockWidgetState extends State with BlockComponentConfigurable, BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin { + BlockComponentBackgroundColorMixin, + DefaultSelectableMixin, + SelectableMixin { // Change the value if the heading block type supports heading levels greater than '3' static const maxVisibleDepth = 6; @@ -95,6 +97,17 @@ class _OutlineBlockWidgetState extends State late EditorState editorState = context.read(); late Stream stream = editorState.transactionStream; + @override + GlobalKey> blockComponentKey = GlobalKey( + debugLabel: OutlineBlockKeys.type, + ); + + @override + GlobalKey> get containerKey => widget.node.key; + + @override + GlobalKey> get forwardKey => widget.node.key; + @override Widget build(BuildContext context) { return StreamBuilder( @@ -102,6 +115,19 @@ class _OutlineBlockWidgetState extends State builder: (context, snapshot) { Widget child = _buildOutlineBlock(); + child = BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + remoteSelection: editorState.remoteSelections, + blockColor: editorState.editorStyle.selectionColor, + selectionAboveBlock: true, + supportTypes: const [ + BlockSelectionType.block, + ], + child: child, + ); + if (UniversalPlatform.isDesktopOrWeb) { if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( @@ -176,6 +202,7 @@ class _OutlineBlockWidgetState extends State } return Container( + key: blockComponentKey, constraints: const BoxConstraints( minHeight: 40.0, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart index a0966465a1..8609b76e70 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart @@ -90,13 +90,8 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( ); Node _buildColumnsNode(EditorState editorState, int columnCount) { - final selection = editorState.selection; - double? width; - if (selection != null) { - final parentNode = editorState.getNodeAtPath(selection.start.path); - if (parentNode != null) { - width = parentNode.rect.width / columnCount - 16; - } - } - return simpleColumnsNode(columnCount: columnCount, width: width); + return simpleColumnsNode( + columnCount: columnCount, + ratio: 1.0 / columnCount, + ); } From ba1dfc6de4098451449018bad61ffb7554de03ad Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 10 Mar 2025 18:10:03 +0800 Subject: [PATCH 096/384] feat: ensure the ai writer block visible when generating result --- .../lib/plugins/document/presentation/editor_page.dart | 6 ++++-- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 83e300146e..b5f32d9f3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -366,7 +366,9 @@ class _AppFlowyEditorPageState extends State contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, - + autoScrollEdgeOffset: UniversalPlatform.isDesktopOrWeb + ? 250 + : appFlowyEditorAutoScrollEdgeOffset, footer: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { @@ -375,7 +377,7 @@ class _AppFlowyEditorPageState extends State }, child: SizedBox( width: double.infinity, - height: UniversalPlatform.isDesktopOrWeb ? 200 : 400, + height: UniversalPlatform.isDesktopOrWeb ? 300 : 400, ), ), dropTargetStyle: AppFlowyDropTargetStyle( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c29671f625..7f66b1d87a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "63e92c1" - resolved-ref: "63e92c19f506dab795d6e1829f82e63a073e7f11" + ref: c802beb + resolved-ref: c802beb8b97ca6261a4ca96db0e1e920e3f3a570 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 29cf86decf..324332df18 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "63e92c1" + ref: "c802beb" appflowy_editor_plugins: git: From 5e593bd36e89adbc56c50877258e3bc29e21a90b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 10 Mar 2025 21:08:28 +0800 Subject: [PATCH 097/384] chore: update appflowy editor version --- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 7f66b1d87a..704ada89e6 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: c802beb - resolved-ref: c802beb8b97ca6261a4ca96db0e1e920e3f3a570 + ref: "5841b4c" + resolved-ref: "5841b4c1bab170da66c52c60977690f2817b8fc4" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 324332df18..71b603bdaa 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "c802beb" + ref: "5841b4c" appflowy_editor_plugins: git: From 22fed1bfbc440c08d3bb263e666e430e0f618399 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 22:48:09 +0800 Subject: [PATCH 098/384] chore: load from env command --- .../settings/ai/local_ai_chat_bloc.dart | 2 +- .../pages/setting_ai_view/plugin_state.dart | 2 +- frontend/resources/translations/en.json | 2 +- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai/src/entities.rs | 4 ++-- .../flowy-ai/src/local_ai/controller.rs | 12 ++++++------ .../rust-lib/flowy-ai/src/local_ai/resource.rs | 18 +++++++++++------- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 16 ++++++++++++++++ 9 files changed, 42 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart index c254246213..14da82ec47 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart @@ -45,7 +45,7 @@ class LocalAISettingPanelBloc ); }, updateAIState: (LocalAIPB pluginState) { - if (pluginState.isAppDownloaded) { + if (pluginState.isExecutableReady) { emit( state.copyWith( runningState: pluginState.state, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index 73fd0f2f42..9e979e9a5a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -223,7 +223,7 @@ class _LackOfResource extends StatelessWidget { desc, maxLines: 3, ), - ) + ), ], ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index de94737ba9..3435fedbb6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -851,7 +851,7 @@ "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is initializing...", + "localAIInitializing": "Local AI is initializing and may take a few minutes, depending on your device", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index be89f620d6..77b46fb6fb 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ab121037b42c9b8f3e874598eb4ee72b29ef46b4#ab121037b42c9b8f3e874598eb4ee72b29ef46b4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=db15d1f1bb387acbb012979951b38e243f35972b#db15d1f1bb387acbb012979951b38e243f35972b" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ab121037b42c9b8f3e874598eb4ee72b29ef46b4#ab121037b42c9b8f3e874598eb4ee72b29ef46b4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=db15d1f1bb387acbb012979951b38e243f35972b#db15d1f1bb387acbb012979951b38e243f35972b" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 0c70b37e09..7ed22256f4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ab121037b42c9b8f3e874598eb4ee72b29ef46b4" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ab121037b42c9b8f3e874598eb4ee72b29ef46b4" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "db15d1f1bb387acbb012979951b38e243f35972b" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "db15d1f1bb387acbb012979951b38e243f35972b" } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index f86cf4c743..06de096270 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -448,7 +448,7 @@ pub enum PendingResourceTypePB { impl From for PendingResourceTypePB { fn from(value: PendingResource) -> Self { match value { - PendingResource::LocalAIAppNotDownloaded { .. } => PendingResourceTypePB::LocalAIAppRes, + PendingResource::PluginExecutableNotReady { .. } => PendingResourceTypePB::LocalAIAppRes, _ => PendingResourceTypePB::AIModel, } } @@ -483,7 +483,7 @@ pub struct LocalAIPB { pub enabled: bool, #[pb(index = 2)] - pub is_app_downloaded: bool, + pub is_executable_ready: bool, #[pb(index = 3, one_of)] pub lack_of_resource: Option, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 4b52851534..7fe2c93dff 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -92,7 +92,7 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - let ready = cloned_llm_res.is_app_downloaded(); + let ready = cloned_llm_res.is_plugin_ready(); let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; let new_state = RunningStatePB::from(state); @@ -103,7 +103,7 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_app_downloaded: ready, + is_executable_ready: ready, lack_of_resource, state: new_state, }) @@ -254,7 +254,7 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let is_app_downloaded = self.resource.is_app_downloaded(); + let is_app_downloaded = self.resource.is_plugin_ready(); let state = self.ai_plugin.get_plugin_running_state(); let lack_of_resource = self.resource.get_lack_of_resource().await; let elapsed = start.elapsed(); @@ -265,7 +265,7 @@ impl LocalAIController { ); LocalAIPB { enabled, - is_app_downloaded, + is_executable_ready: is_app_downloaded, state: RunningStatePB::from(state), lack_of_resource, } @@ -439,7 +439,7 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_app_downloaded: true, + is_executable_ready: true, state: RunningStatePB::Stopped, lack_of_resource: None, }) @@ -467,7 +467,7 @@ async fn initialize_ai_plugin( ) .payload(LocalAIPB { enabled: true, - is_app_downloaded: true, + is_executable_ready: true, state: RunningStatePB::ReadyToRun, lack_of_resource: lack_of_resource.clone(), }) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 1825ebea50..2bcdd8c65f 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,7 +5,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -use crate::local_ai::watch::ollama_plugin_path; +use crate::local_ai::watch::{ollama_plugin_command_available, ollama_plugin_path}; #[cfg(target_os = "macos")] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ @@ -37,7 +37,7 @@ pub enum WatchDiskEvent { } pub enum PendingResource { - LocalAIAppNotDownloaded, + PluginExecutableNotReady, OllamaServerNotReady, MissingModel(String), } @@ -45,7 +45,7 @@ pub enum PendingResource { impl PendingResource { pub fn desc(self) -> String { match self { - PendingResource::LocalAIAppNotDownloaded => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), + PendingResource::PluginExecutableNotReady => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), PendingResource::OllamaServerNotReady => "Ollama is not ready. Please follow the instructions to install Ollama".to_string(), PendingResource::MissingModel(model) => format!("Cannot find the model: {}. Please use the ollama pull command to install the model", model), } @@ -123,8 +123,8 @@ impl LocalAIResourceController { } } - pub fn is_app_downloaded(&self) -> bool { - ollama_plugin_path().exists() + pub fn is_plugin_ready(&self) -> bool { + ollama_plugin_path().exists() || ollama_plugin_command_available() } pub async fn get_plugin_download_link(&self) -> FlowyResult { @@ -164,8 +164,11 @@ impl LocalAIResourceController { let mut resources = vec![]; let app_path = ollama_plugin_path(); if !app_path.exists() { - trace!("[LLM Resource] offline app not found: {:?}", app_path); - resources.push(PendingResource::LocalAIAppNotDownloaded); + if !ollama_plugin_command_available() { + trace!("[LLM Resource] offline app not found: {:?}", app_path); + resources.push(PendingResource::PluginExecutableNotReady); + return Ok(resources); + } } let setting = self.get_llm_setting(); @@ -254,6 +257,7 @@ impl LocalAIResourceController { let mut config = OllamaPluginConfig::new( bin_path, + "ollama_ai_plugin".to_string(), llm_setting.chat_model_name.clone(), llm_setting.embedding_model_name.clone(), Some(llm_setting.ollama_server_url.clone()), diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 11063e43d6..b9db41d663 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -1,6 +1,7 @@ use crate::local_ai::resource::WatchDiskEvent; use flowy_error::{FlowyError, FlowyResult}; use std::path::PathBuf; +use std::process::Command; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::{error, trace}; @@ -102,3 +103,18 @@ pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) } } + +pub(crate) fn ollama_plugin_command_available() -> bool { + let command = if cfg!(windows) { "where" } else { "command" }; + let args = if cfg!(windows) { + vec!["/c", "where", "ollama_ai_plugin"] + } else { + vec!["-v", "ollama_ai_plugin"] + }; + + let output = Command::new(command).args(args).output(); + match output { + Ok(output) => !output.stdout.is_empty(), + Err(_) => false, + } +} From 59139ff323d50669baf43395c380a18ecabbebb2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 10 Mar 2025 23:40:28 +0800 Subject: [PATCH 099/384] chore: catch panic --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai/src/entities.rs | 2 +- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 77b46fb6fb..39945922c1 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=db15d1f1bb387acbb012979951b38e243f35972b#db15d1f1bb387acbb012979951b38e243f35972b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=eace64bb0e4d2b781c74bbfc77ed7f1de0559b35#eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=db15d1f1bb387acbb012979951b38e243f35972b#db15d1f1bb387acbb012979951b38e243f35972b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=eace64bb0e4d2b781c74bbfc77ed7f1de0559b35#eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 7ed22256f4..65cc255f75 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "db15d1f1bb387acbb012979951b38e243f35972b" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "db15d1f1bb387acbb012979951b38e243f35972b" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 06de096270..9ffa5f3256 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -483,7 +483,7 @@ pub struct LocalAIPB { pub enabled: bool, #[pb(index = 2)] - pub is_executable_ready: bool, + pub is_plugin_executable_ready: bool, #[pb(index = 3, one_of)] pub lack_of_resource: Option, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 7fe2c93dff..9c70451e27 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -103,7 +103,7 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_executable_ready: ready, + is_plugin_executable_ready: ready, lack_of_resource, state: new_state, }) @@ -265,7 +265,7 @@ impl LocalAIController { ); LocalAIPB { enabled, - is_executable_ready: is_app_downloaded, + is_plugin_executable_ready: is_app_downloaded, state: RunningStatePB::from(state), lack_of_resource, } @@ -439,7 +439,7 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_executable_ready: true, + is_plugin_executable_ready: true, state: RunningStatePB::Stopped, lack_of_resource: None, }) @@ -467,7 +467,7 @@ async fn initialize_ai_plugin( ) .payload(LocalAIPB { enabled: true, - is_executable_ready: true, + is_plugin_executable_ready: true, state: RunningStatePB::ReadyToRun, lack_of_resource: lack_of_resource.clone(), }) From 940db704476ac05f0a4a7409b26316f2cc30f4a3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 00:27:55 +0800 Subject: [PATCH 100/384] chore: fix build --- .../workspace/application/settings/ai/local_ai_chat_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart index 14da82ec47..ed55ffcf9b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart @@ -45,7 +45,7 @@ class LocalAISettingPanelBloc ); }, updateAIState: (LocalAIPB pluginState) { - if (pluginState.isExecutableReady) { + if (pluginState.isPluginExecutableReady) { emit( state.copyWith( runningState: pluginState.state, From e7cd90b6ab9b88bf769e76b6b6192a5c21fe8849 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 09:14:11 +0800 Subject: [PATCH 101/384] chore: update commit --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 39945922c1..e44a0445ed 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=eace64bb0e4d2b781c74bbfc77ed7f1de0559b35#eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8657a3ce64948c672f548ca0dd9f0257db9c7156#8657a3ce64948c672f548ca0dd9f0257db9c7156" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=eace64bb0e4d2b781c74bbfc77ed7f1de0559b35#eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8657a3ce64948c672f548ca0dd9f0257db9c7156#8657a3ce64948c672f548ca0dd9f0257db9c7156" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 65cc255f75..ce5b04772f 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "eace64bb0e4d2b781c74bbfc77ed7f1de0559b35" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8657a3ce64948c672f548ca0dd9f0257db9c7156" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8657a3ce64948c672f548ca0dd9f0257db9c7156" } From bd06e1d5598f07c53e2a2c09a161180444ccce16 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 09:32:20 +0800 Subject: [PATCH 102/384] chore: clippy --- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 2bcdd8c65f..c0dd3c2d38 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -163,12 +163,10 @@ impl LocalAIResourceController { pub async fn calculate_pending_resources(&self) -> FlowyResult> { let mut resources = vec![]; let app_path = ollama_plugin_path(); - if !app_path.exists() { - if !ollama_plugin_command_available() { - trace!("[LLM Resource] offline app not found: {:?}", app_path); - resources.push(PendingResource::PluginExecutableNotReady); - return Ok(resources); - } + if !app_path.exists() && !ollama_plugin_command_available() { + trace!("[LLM Resource] offline app not found: {:?}", app_path); + resources.push(PendingResource::PluginExecutableNotReady); + return Ok(resources); } let setting = self.get_llm_setting(); From 667d15c627ac7eedb0e953d2faf1a6e05f629675 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 10:31:08 +0800 Subject: [PATCH 103/384] fix: ai writer gestures (#7499) --- .../ai/ai_writer_block_component.dart | 10 +++-- .../widgets/ai_writer_gesture_detector.dart | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 3835ff7fd4..ee6cf16909 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -18,6 +18,7 @@ import 'operations/ai_writer_cubit.dart'; import 'operations/ai_writer_entities.dart'; import 'operations/ai_writer_node_extension.dart'; import 'suggestion_action_bar.dart'; +import 'widgets/ai_writer_gesture_detector.dart'; class AiWriterBlockKeys { const AiWriterBlockKeys._(); @@ -168,10 +169,11 @@ class _AIWriterBlockComponentState extends State { children: [ BlocBuilder( builder: (context, state) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => onTapOutside(), - onTapDown: (_) => onTapOutside(), + return AiWriterGestureDetector( + behavior: state is GeneratingAiWriterState + ? HitTestBehavior.opaque + : HitTestBehavior.translucent, + onPointerEvent: () => onTapOutside(), ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart new file mode 100644 index 0000000000..6b2ffd4692 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart @@ -0,0 +1,40 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class AiWriterGestureDetector extends StatelessWidget { + const AiWriterGestureDetector({ + super.key, + required this.behavior, + required this.onPointerEvent, + this.child, + }); + + final HitTestBehavior behavior; + final void Function() onPointerEvent; + final Widget? child; + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: behavior, + gestures: { + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (instance) { + instance + ..onTap = onPointerEvent + ..onTapDown = (_) => onPointerEvent(); + }, + ), + ImmediateMultiDragGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + ImmediateMultiDragGestureRecognizer>( + () => ImmediateMultiDragGestureRecognizer(), (instance) { + instance.onStart = (offset) => null; + }), + }, + child: child, + ); + } +} From eb0ed1ad86fc47fc2f074c7b0dd3a797f9699f1e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:38:20 +0800 Subject: [PATCH 104/384] fix: don't allow selection of text in related questions or loading (#7500) --- .../lib/ai/widgets/loading_indicator.dart | 84 ++++++++++--------- .../presentation/chat_related_question.dart | 56 +++++++------ 2 files changed, 72 insertions(+), 68 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart index e21ea95ac1..3a9c96b255 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart @@ -16,48 +16,50 @@ class AILoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return SizedBox( - height: 20, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: FlowyText( - text, - color: Theme.of(context).hintColor, + return SelectionContainer.disabled( + child: SizedBox( + height: 20, + child: SeparatedRow( + separatorBuilder: () => const HSpace(4), + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: FlowyText( + text, + color: Theme.of(context).hintColor, + ), ), - ), - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], + buildDot(const Color(0xFF9327FF)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(duration: slice * 2, begin: 0, end: 0), + buildDot(const Color(0xFFFB006D)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: 0) + .then() + .slideY(begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(begin: 0, end: 0), + buildDot(const Color(0xFFFFCE00)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice * 2, begin: 0, end: 0) + .then() + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0), + ], + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index 9e4cc603b0..cc21cfc80e 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -21,33 +21,35 @@ class RelatedQuestionList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: relatedQuestions.length + 1, - padding: - const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin, - separatorBuilder: (context, index) => const VSpace(4.0), - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.chat_relatedQuestion.tr(), - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w600, - ), - ); - } else { - return Align( - alignment: AlignmentDirectional.centerStart, - child: RelatedQuestionItem( - question: relatedQuestions[index - 1], - onQuestionSelected: onQuestionSelected, - ), - ); - } - }, + return SelectionContainer.disabled( + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: relatedQuestions.length + 1, + padding: + const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin, + separatorBuilder: (context, index) => const VSpace(4.0), + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText( + LocaleKeys.chat_relatedQuestion.tr(), + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w600, + ), + ); + } else { + return Align( + alignment: AlignmentDirectional.centerStart, + child: RelatedQuestionItem( + question: relatedQuestions[index - 1], + onQuestionSelected: onQuestionSelected, + ), + ); + } + }, + ), ); } } From 702a486ccee332812ffa47d0e5e36a72792baf69 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 12:55:19 +0800 Subject: [PATCH 105/384] chore: find windows exe --- frontend/rust-lib/Cargo.lock | 86 ++++++++++++++++--- .../build-tool/flowy-codegen/Cargo.toml | 2 +- frontend/rust-lib/flowy-ai/Cargo.toml | 6 ++ .../rust-lib/flowy-ai/src/local_ai/watch.rs | 79 ++++++++++++++--- 4 files changed, 149 insertions(+), 24 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e44a0445ed..07a1b8ae52 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -871,11 +871,12 @@ dependencies = [ [[package]] name = "cmd_lib" -version = "1.3.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62" +checksum = "371c15a3c178d0117091bd84414545309ca979555b1aad573ef591ad58818d41" dependencies = [ "cmd_lib_macros", + "env_logger 0.10.2", "faccess", "lazy_static", "log", @@ -884,14 +885,14 @@ dependencies = [ [[package]] name = "cmd_lib_macros" -version = "1.3.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0" +checksum = "cb844bd05be34d91eb67101329aeba9d3337094c04fd8507d821db7ebb488eaf" dependencies = [ - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.94", ] [[package]] @@ -1791,6 +1792,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2043,6 +2057,7 @@ dependencies = [ "tracing-subscriber", "uuid", "validator 0.18.1", + "winreg 0.55.0", "zip 2.2.0", "zip-extensions", ] @@ -3078,6 +3093,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + [[package]] name = "hex" version = "0.4.3" @@ -3648,6 +3669,17 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.0", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -4328,7 +4360,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.2", "libc", ] @@ -4418,12 +4450,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "0.9.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -5155,7 +5187,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger", + "env_logger 0.8.4", "log", "rand 0.8.5", ] @@ -5220,7 +5252,7 @@ dependencies = [ "once_cell", "socket2 0.5.5", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5562,7 +5594,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots 0.25.2", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -6735,6 +6767,15 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.1.17" @@ -7862,6 +7903,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8002,6 +8052,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml index 27d36c310c..ae07268ee9 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml +++ b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml @@ -12,7 +12,7 @@ serde_json.workspace = true flowy-ast.workspace = true quote = "1.0" -cmd_lib = { version = "1.3.0", optional = true } +cmd_lib = { version = "1.9.5", optional = true } protoc-rust = { version = "2.28.0", optional = true } #protobuf-codegen = { version = "3.7.1" } walkdir = { version = "2", optional = true } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index c2f94d509f..145ce41d3b 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -47,9 +47,15 @@ pin-project = "1.1.5" flowy-storage-pub = { workspace = true } collab-integrate.workspace = true + [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] notify = "6.1.1" +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "0.55" + +#cmd_lib = { version = "1.9.5" } + [dev-dependencies] dotenv = "0.15.0" uuid.workspace = true diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index b9db41d663..12d7a4c5b6 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -5,6 +5,9 @@ use std::process::Command; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::{error, trace}; +#[cfg(windows)] +use winreg::{enums::*, RegKey}; + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[allow(dead_code)] pub struct WatchContext { @@ -105,16 +108,72 @@ pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { } pub(crate) fn ollama_plugin_command_available() -> bool { - let command = if cfg!(windows) { "where" } else { "command" }; - let args = if cfg!(windows) { - vec!["/c", "where", "ollama_ai_plugin"] - } else { - vec!["-v", "ollama_ai_plugin"] - }; + if cfg!(windows) { + #[cfg(windows)] + { + // 1. Try "where" command first + let output = Command::new("cmd") + .args(["/C", "where", "ollama_ai_plugin"]) + .output(); + if let Ok(output) = output { + if !output.stdout.is_empty() { + return true; + } + } - let output = Command::new(command).args(args).output(); - match output { - Ok(output) => !output.stdout.is_empty(), - Err(_) => false, + // 2. Fallback: Check registry PATH for the executable + let path_dirs = get_windows_path_dirs(); + let plugin_exe = "ollama_ai_plugin.exe"; // Adjust name if needed + + path_dirs.iter().any(|dir| { + let full_path = std::path::Path::new(dir).join(plugin_exe); + full_path.exists() + }) + } + + #[cfg(not(windows))] + false + } else { + let output = Command::new("command") + .args(&["-v", "ollama_ai_plugin"]) + .output(); + match output { + Ok(o) => !o.stdout.is_empty(), + _ => false, + } + } +} + +#[cfg(windows)] +fn get_windows_path_dirs() -> Vec { + let mut paths = Vec::new(); + + // Check HKEY_CURRENT_USER\Environment + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + if let Ok(env) = hkcu.open_subkey("Environment") { + if let Ok(path) = env.get_value::("Path") { + paths.extend(path.split(';').map(|s| s.trim().to_string())); + } + } + + // Check HKEY_LOCAL_MACHINE\SYSTEM\...\Environment + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + if let Ok(env) = hklm.open_subkey(r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment") + { + if let Ok(path) = env.get_value::("Path") { + paths.extend(path.split(';').map(|s| s.trim().to_string())); + } + } + paths +} + +#[cfg(test)] +mod tests { + use crate::local_ai::watch::ollama_plugin_command_available; + + #[test] + fn test_command_import() { + let result = ollama_plugin_command_available(); + println!("ollama plugin exist: {:?}", result); } } From 6ba7f93f69ffdf7596509391d6f41a3d315335db Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 13:14:47 +0800 Subject: [PATCH 106/384] chore: find plugin load --- .../rust-lib/flowy-ai/src/local_ai/resource.rs | 14 ++++++++------ frontend/rust-lib/flowy-ai/src/local_ai/watch.rs | 4 ++++ frontend/rust-lib/flowy-error/src/code.rs | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index c0dd3c2d38..bca27488cd 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,7 +5,9 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -use crate::local_ai::watch::{ollama_plugin_command_available, ollama_plugin_path}; +use crate::local_ai::watch::{ + is_plugin_ready, ollama_plugin_command_available, ollama_plugin_path, +}; #[cfg(target_os = "macos")] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ @@ -236,14 +238,14 @@ impl LocalAIResourceController { let llm_setting = self.get_llm_setting(); let bin_path = match get_operating_system() { OperatingSystem::MacOS | OperatingSystem::Windows => { - let path = ollama_plugin_path(); - if !path.exists() { + if !is_plugin_ready() { return Err(FlowyError::new( - ErrorCode::AIOfflineNotInstalled, - format!("AppFlowy Offline not installed at path: {:?}", path), + ErrorCode::AppFlowyLAINotReady, + "AppFlowyLAI not found", )); } - path + + ollama_plugin_path() }, _ => { return Err( diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 12d7a4c5b6..a6ad07563b 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -84,6 +84,10 @@ pub(crate) fn offline_app_path() -> PathBuf { PathBuf::new() } +pub fn is_plugin_ready() -> bool { + ollama_plugin_path().exists() || ollama_plugin_command_available() +} + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { #[cfg(target_os = "windows")] diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index e97393d094..85822f5e02 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -302,8 +302,8 @@ pub enum ErrorCode { #[error("Unsupported file format")] UnsupportedFileFormat = 104, - #[error("AI offline not started")] - AIOfflineNotInstalled = 105, + #[error("AppFlowy LAI not ready")] + AppFlowyLAINotReady = 105, #[error("Invalid Request")] InvalidRequest = 106, From 83c53188e346e39c0ff9f31e7648f77d1e5e0ab9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 13:21:58 +0800 Subject: [PATCH 107/384] chore: clippy --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 5 +++-- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 10 ++-------- frontend/rust-lib/flowy-ai/src/local_ai/watch.rs | 2 +- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 07a1b8ae52..018150db65 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8657a3ce64948c672f548ca0dd9f0257db9c7156#8657a3ce64948c672f548ca0dd9f0257db9c7156" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a76364694d696767488c8b12e210eefa58453c89#a76364694d696767488c8b12e210eefa58453c89" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8657a3ce64948c672f548ca0dd9f0257db9c7156#8657a3ce64948c672f548ca0dd9f0257db9c7156" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a76364694d696767488c8b12e210eefa58453c89#a76364694d696767488c8b12e210eefa58453c89" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ce5b04772f..9d3446d7b1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8657a3ce64948c672f548ca0dd9f0257db9c7156" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8657a3ce64948c672f548ca0dd9f0257db9c7156" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a76364694d696767488c8b12e210eefa58453c89" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a76364694d696767488c8b12e210eefa58453c89" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 9c70451e27..dd296158e4 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -13,6 +13,7 @@ use futures::Sink; use lib_infra::async_trait::async_trait; use std::collections::HashMap; +use crate::local_ai::watch::is_plugin_ready; use crate::stream_message::StreamMessage; use appflowy_local_ai::ollama_plugin::OllamaAIPlugin; use arc_swap::ArcSwapOption; @@ -92,7 +93,7 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - let ready = cloned_llm_res.is_plugin_ready(); + let ready = is_plugin_ready(); let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; let new_state = RunningStatePB::from(state); @@ -254,7 +255,7 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let is_app_downloaded = self.resource.is_plugin_ready(); + let is_app_downloaded = is_plugin_ready(); let state = self.ai_plugin.get_plugin_running_state(); let lack_of_resource = self.resource.get_lack_of_resource().await; let elapsed = start.elapsed(); diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index bca27488cd..1380ed38ec 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,9 +5,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -use crate::local_ai::watch::{ - is_plugin_ready, ollama_plugin_command_available, ollama_plugin_path, -}; +use crate::local_ai::watch::{is_plugin_ready, ollama_plugin_path}; #[cfg(target_os = "macos")] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ @@ -125,10 +123,6 @@ impl LocalAIResourceController { } } - pub fn is_plugin_ready(&self) -> bool { - ollama_plugin_path().exists() || ollama_plugin_command_available() - } - pub async fn get_plugin_download_link(&self) -> FlowyResult { let ai_config = self.get_local_ai_configuration().await?; Ok(ai_config.plugin.url) @@ -165,7 +159,7 @@ impl LocalAIResourceController { pub async fn calculate_pending_resources(&self) -> FlowyResult> { let mut resources = vec![]; let app_path = ollama_plugin_path(); - if !app_path.exists() && !ollama_plugin_command_available() { + if !is_plugin_ready() { trace!("[LLM Resource] offline app not found: {:?}", app_path); resources.push(PendingResource::PluginExecutableNotReady); return Ok(resources); diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index a6ad07563b..a5707bab72 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -139,7 +139,7 @@ pub(crate) fn ollama_plugin_command_available() -> bool { false } else { let output = Command::new("command") - .args(&["-v", "ollama_ai_plugin"]) + .args(["-v", "ollama_ai_plugin"]) .output(); match output { Ok(o) => !o.stdout.is_empty(), From 96608bd005eb0aeb58553b54ae5d78743021ddcf Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:06:55 +0800 Subject: [PATCH 108/384] chore: hide predefined format section when using local ai --- .../lib/ai/service/ai_prompt_input_bloc.dart | 46 +++++++-------- .../desktop_prompt_text_field.dart | 59 ++++++++++--------- .../chat_input/mobile_chat_input.dart | 4 +- .../settings/ai/ollama_setting_bloc.dart | 1 + frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++-------- 5 files changed, 81 insertions(+), 75 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 2cad319cdd..7751a3357e 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -36,35 +36,31 @@ class AIPromptInputBloc extends Bloc { on( (event, emit) { event.when( - updateAIState: (LocalAIPB localAIState) { + updateAIState: (localAIState) { + AiType aiType = localAIState.enabled ? AiType.local : AiType.cloud; + bool supportChatWithFile = + aiType.isLocal && localAIState.state == RunningStatePB.Running; + if (localAIState.hasLackOfResource()) { - emit( - state.copyWith( - aiType: AIType.appflowyAI, - supportChatWithFile: false, - localAIState: localAIState, - ), - ); - return; + aiType = AiType.cloud; + supportChatWithFile = false; } - // Only user enable chat with file and the plugin is already running - final supportChatWithFile = localAIState.enabled && - localAIState.state == RunningStatePB.Running; - - final aiType = - localAIState.enabled ? AIType.localAI : AIType.appflowyAI; + final showPredefinedFormats = + aiType.isCloud && state.showPredefinedFormats; emit( state.copyWith( aiType: aiType, supportChatWithFile: supportChatWithFile, localAIState: localAIState, + showPredefinedFormats: showPredefinedFormats, ), ); }, toggleShowPredefinedFormat: () { + final showPredefinedFormats = !state.showPredefinedFormats; final predefinedFormat = - !state.showPredefinedFormats && state.predefinedFormat == null + showPredefinedFormats && state.predefinedFormat == null ? PredefinedFormat( imageFormat: ImageFormat.text, textFormat: TextFormat.paragraph, @@ -72,12 +68,15 @@ class AIPromptInputBloc extends Bloc { : null; emit( state.copyWith( - showPredefinedFormats: !state.showPredefinedFormats, + showPredefinedFormats: showPredefinedFormats, predefinedFormat: predefinedFormat, ), ); }, updatePredefinedFormat: (format) { + if (!state.showPredefinedFormats) { + return; + } emit(state.copyWith(predefinedFormat: format)); }, attachFile: (filePath, fileName) { @@ -176,7 +175,7 @@ class AIPromptInputEvent with _$AIPromptInputEvent { @freezed class AIPromptInputState with _$AIPromptInputState { const factory AIPromptInputState({ - required AIType aiType, + required AiType aiType, required bool supportChatWithFile, required bool showPredefinedFormats, required PredefinedFormat? predefinedFormat, @@ -187,7 +186,7 @@ class AIPromptInputState with _$AIPromptInputState { factory AIPromptInputState.initial(PredefinedFormat? format) => AIPromptInputState( - aiType: AIType.appflowyAI, + aiType: AiType.cloud, supportChatWithFile: false, showPredefinedFormats: format != null, predefinedFormat: format, @@ -197,9 +196,10 @@ class AIPromptInputState with _$AIPromptInputState { ); } -enum AIType { - appflowyAI, - localAI; +enum AiType { + cloud, + local; - bool get isLocalAI => this == localAI; + bool get isCloud => this == cloud; + bool get isLocal => this == local; } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 0905e9b6bc..5bb7eb1c0d 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -283,10 +283,10 @@ class _DesktopPromptInputState extends State { return; } - // handle text and selection changes ONLY when mentioning a page - // disable mention return; + + // handle text and selection changes ONLY when mentioning a page // ignore: dead_code if (!overlayController.isShowing || inputControlCubit.filterStartPosition == -1) { @@ -384,8 +384,8 @@ class _DesktopPromptInputState extends State { contentPadding: calculateContentPadding(state.showPredefinedFormats), hintText: switch (state.aiType) { - AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), - AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() + AiType.cloud => LocaleKeys.chat_inputMessageHint.tr(), + AiType.local => LocaleKeys.chat_inputLocalAIMessageHint.tr() }, ); }, @@ -578,29 +578,34 @@ class _PromptBottomActions extends StatelessWidget { child: _sendButton(), ); } - return Row( - children: [ - _predefinedFormatButton(), - const Spacer(), - if (state.aiType == AIType.appflowyAI) ...[ - _selectSourcesButton(context), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - // _mentionButton(context), - // const HSpace( - // DesktopAIPromptSizes.actionBarButtonSpacing, - // ), - if (state.supportChatWithFile) ...[ - _attachmentButton(context), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - _sendButton(), - ], - ); + return state.aiType.isLocal + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (state.supportChatWithFile) ...[ + _attachmentButton(context), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + ], + _sendButton(), + ], + ) + : Row( + children: [ + _predefinedFormatButton(), + const Spacer(), + _selectSourcesButton(context), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + // _mentionButton(context), + // const HSpace( + // DesktopAIPromptSizes.actionBarButtonSpacing, + // ), + _sendButton(), + ], + ); }, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart index 4d5cd82098..0aa7465dfb 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart @@ -268,8 +268,8 @@ class _MobileChatInputState extends State { focusedBorder: InputBorder.none, contentPadding: MobileAIPromptSizes.textFieldContentPadding, hintText: switch (state.aiType) { - AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), - AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() + AiType.cloud => LocaleKeys.chat_inputMessageHint.tr(), + AiType.local => LocaleKeys.chat_inputLocalAIMessageHint.tr() }, hintStyle: inputHintTextStyle(context), isCollapsed: true, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart index 6c9ebe2a3f..8555d8cdc8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart @@ -8,6 +8,7 @@ import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:equatable/equatable.dart'; + part 'ollama_setting_bloc.freezed.dart'; class OllamaSettingBloc extends Bloc { diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 From 01e5817b2437b709fe04c2c4182761aa5959b6c4 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:23:57 +0800 Subject: [PATCH 109/384] chore: code cleanup --- .../ai/local_ai_chat_toggle_bloc.dart | 78 ------------------- ....dart => local_ai_setting_panel_bloc.dart} | 24 +++--- .../pages/setting_ai_view/init_local_ai.dart | 2 +- .../setting_ai_view/local_ai_setting.dart | 7 +- .../local_ai_setting_panel.dart | 60 +++++--------- .../pages/setting_ai_view/ollma_setting.dart | 1 + .../setting_ai_view/settings_ai_view.dart | 29 ++++--- 7 files changed, 45 insertions(+), 156 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart rename frontend/appflowy_flutter/lib/workspace/application/settings/ai/{local_ai_chat_bloc.dart => local_ai_setting_panel_bloc.dart} (87%) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart deleted file mode 100644 index f533778f9b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'local_ai_chat_toggle_bloc.freezed.dart'; - -class LocalAIChatToggleBloc - extends Bloc { - LocalAIChatToggleBloc() : super(const LocalAIChatToggleState()) { - on(_handleEvent); - } - - Future _handleEvent( - LocalAIChatToggleEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIState().send(); - _handleResult(emit, result); - }, - handleResult: (result) { - _handleResult(emit, result); - }, - ); - } - - void _handleResult( - Emitter emit, - FlowyResult result, - ) { - result.fold( - (localAI) { - emit( - state.copyWith( - pageIndicator: - LocalAIChatToggleStateIndicator.ready(localAI.enabled), - ), - ); - }, - (err) { - emit( - state.copyWith( - pageIndicator: LocalAIChatToggleStateIndicator.error(err), - ), - ); - }, - ); - } -} - -@freezed -class LocalAIChatToggleEvent with _$LocalAIChatToggleEvent { - const factory LocalAIChatToggleEvent.started() = _Started; - const factory LocalAIChatToggleEvent.handleResult( - FlowyResult result, - ) = _HandleResult; -} - -@freezed -class LocalAIChatToggleState with _$LocalAIChatToggleState { - const factory LocalAIChatToggleState({ - @Default(LocalAIChatToggleStateIndicator.loading()) - LocalAIChatToggleStateIndicator pageIndicator, - }) = _LocalAIChatToggleState; -} - -@freezed -class LocalAIChatToggleStateIndicator with _$LocalAIChatToggleStateIndicator { - const factory LocalAIChatToggleStateIndicator.error(FlowyError error) = - _OnError; - const factory LocalAIChatToggleStateIndicator.ready(bool isEnabled) = _Ready; - const factory LocalAIChatToggleStateIndicator.loading() = _Loading; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart similarity index 87% rename from frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart rename to frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart index ed55ffcf9b..5ed82c601a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart @@ -8,13 +8,15 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'local_ai_chat_bloc.freezed.dart'; +part 'local_ai_setting_panel_bloc.freezed.dart'; class LocalAISettingPanelBloc extends Bloc { LocalAISettingPanelBloc() : listener = LocalLLMListener(), super(const LocalAISettingPanelState()) { + on(_handleEvent); + listener.start( stateCallback: (newState) { if (!isClosed) { @@ -23,7 +25,14 @@ class LocalAISettingPanelBloc }, ); - on(_handleEvent); + AIEventGetLocalAIState().send().fold( + (localAIState) { + if (!isClosed) { + add(LocalAISettingPanelEvent.updateAIState(localAIState)); + } + }, + Log.error, + ); } final LocalLLMListener listener; @@ -34,16 +43,6 @@ class LocalAISettingPanelBloc Emitter emit, ) async { event.when( - started: () { - AIEventGetLocalAIState().send().fold( - (localAIState) { - if (!isClosed) { - add(LocalAISettingPanelEvent.updateAIState(localAIState)); - } - }, - Log.error, - ); - }, updateAIState: (LocalAIPB pluginState) { if (pluginState.isPluginExecutableReady) { emit( @@ -72,7 +71,6 @@ class LocalAISettingPanelBloc @freezed class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent { - const factory LocalAISettingPanelEvent.started() = _Started; const factory LocalAISettingPanelEvent.updateAIState( LocalAIPB aiState, ) = _UpdateAIState; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart index 3f557eff91..cf9410dad9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 2a88008565..46935a2450 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -12,14 +12,9 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class LocalAISetting extends StatefulWidget { +class LocalAISetting extends StatelessWidget { const LocalAISetting({super.key}); - @override - State createState() => _LocalAISettingState(); -} - -class _LocalAISettingState extends State { @override Widget build(BuildContext context) { return BlocBuilder( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart index 6b95f2e9d2..c379cf6089 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -1,5 +1,4 @@ -import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -12,14 +11,8 @@ class LocalAISettingPanel extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => LocalAISettingPanelBloc()), - BlocProvider( - create: (context) => LocalAIChatToggleBloc() - ..add(const LocalAIChatToggleEvent.started()), - ), - ], + return BlocProvider( + create: (context) => LocalAISettingPanelBloc(), child: ExpandableNotifier( initialExpanded: true, child: ExpandablePanel( @@ -34,28 +27,24 @@ class LocalAISettingPanel extends StatelessWidget { collapsed: const SizedBox.shrink(), expanded: Padding( padding: const EdgeInsets.symmetric(vertical: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: BlocBuilder( - builder: (context, state) { + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ // If the progress indicator is startLocalAIApp, then don't show the LLM model. if (state.progressIndicator == - const LocalAIProgress.downloadLocalAIApp()) { - return const SizedBox.shrink(); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - OllamaSettingPage(), - VSpace(6), - _LocalAIStateWidget(), - ], - ); - } - }, - ), - ], + const LocalAIProgress.downloadLocalAIApp()) + const SizedBox.shrink() + else ...[ + OllamaSettingPage(), + VSpace(6), + PluginStateIndicator(), + ], + ], + ); + }, ), ), ), @@ -63,16 +52,3 @@ class LocalAISettingPanel extends StatelessWidget { ); } } - -class _LocalAIStateWidget extends StatelessWidget { - const _LocalAIStateWidget(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return const PluginStateIndicator(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart index 04bd27372a..bc190b895a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart @@ -29,6 +29,7 @@ class OllamaSettingPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListView.separated( + physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: state.inputItems.length, separatorBuilder: (_, __) => const VSpace(10), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index a6181ba016..7e795306fb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -13,7 +13,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -150,23 +149,21 @@ class _LocalAIOnBoarding extends StatelessWidget { child: BlocBuilder( builder: (context, state) { // Show the local AI settings if the user has purchased the AI Local plan - if (kDebugMode || state.isPurchaseAILocal) { + if (state.isPurchaseAILocal) { return const LocalAISetting(); + } else if (currentWorkspaceMemberRole?.isOwner ?? false) { + // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan + return _UpgradeToAILocalPlan( + onTap: () { + context.read().add( + const LocalAIOnBoardingEvent.addSubscription( + SubscriptionPlanPB.AiLocal, + ), + ); + }, + ); } else { - if (currentWorkspaceMemberRole?.isOwner ?? false) { - // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan - return _UpgradeToAILocalPlan( - onTap: () { - context.read().add( - const LocalAIOnBoardingEvent.addSubscription( - SubscriptionPlanPB.AiLocal, - ), - ); - }, - ); - } else { - return const _AskOwnerUpgradeToLocalAI(); - } + return const _AskOwnerUpgradeToLocalAI(); } }, ), From f8f9c3404a25616b13c3c4c263532cefac156a8a Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:05:30 +0800 Subject: [PATCH 110/384] chore: show text options still --- .../lib/ai/service/ai_prompt_input_bloc.dart | 3 - .../desktop_prompt_text_field.dart | 56 +++++++++---------- .../predefined_format_buttons.dart | 15 +++-- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 7751a3357e..9d42370ed4 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -46,14 +46,11 @@ class AIPromptInputBloc extends Bloc { supportChatWithFile = false; } - final showPredefinedFormats = - aiType.isCloud && state.showPredefinedFormats; emit( state.copyWith( aiType: aiType, supportChatWithFile: supportChatWithFile, localAIState: localAIState, - showPredefinedFormats: showPredefinedFormats, ), ); }, diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 5bb7eb1c0d..b52898880c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -148,14 +148,13 @@ class _DesktopPromptInputState extends State { start: 8.0, ), child: ChangeFormatBar( + showImageFormats: state.aiType.isCloud, predefinedFormat: state.predefinedFormat, spacing: 4.0, onSelectPredefinedFormat: (format) => context.read().add( AIPromptInputEvent - .updatePredefinedFormat( - format, - ), + .updatePredefinedFormat(format), ), ), ), @@ -578,34 +577,29 @@ class _PromptBottomActions extends StatelessWidget { child: _sendButton(), ); } - return state.aiType.isLocal - ? Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (state.supportChatWithFile) ...[ - _attachmentButton(context), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - _sendButton(), - ], - ) - : Row( - children: [ - _predefinedFormatButton(), - const Spacer(), - _selectSourcesButton(context), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - // _mentionButton(context), - // const HSpace( - // DesktopAIPromptSizes.actionBarButtonSpacing, - // ), - _sendButton(), - ], - ); + return Row( + children: [ + _predefinedFormatButton(), + const Spacer(), + if (state.aiType.isCloud) ...[ + _selectSourcesButton(context), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + ], + // _mentionButton(context), + // const HSpace( + // DesktopAIPromptSizes.actionBarButtonSpacing, + // ), + if (state.supportChatWithFile) ...[ + _attachmentButton(context), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + ], + _sendButton(), + ], + ); }, ), ); diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart index 189882ae4d..253d6f70b7 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -48,25 +48,30 @@ class ChangeFormatBar extends StatelessWidget { required this.predefinedFormat, required this.spacing, required this.onSelectPredefinedFormat, + this.showImageFormats = false, }); final PredefinedFormat? predefinedFormat; final double spacing; final void Function(PredefinedFormat) onSelectPredefinedFormat; + final bool showImageFormats; @override Widget build(BuildContext context) { + final showTextFormats = predefinedFormat?.imageFormat.hasText ?? true; return SizedBox( height: DesktopAIPromptSizes.predefinedFormatButtonHeight, child: SeparatedRow( mainAxisSize: MainAxisSize.min, separatorBuilder: () => HSpace(spacing), children: [ - _buildFormatButton(context, ImageFormat.text), - _buildFormatButton(context, ImageFormat.textAndImage), - _buildFormatButton(context, ImageFormat.image), - if (predefinedFormat?.imageFormat.hasText ?? true) ...[ - _buildDivider(), + if (showImageFormats) ...[ + _buildFormatButton(context, ImageFormat.text), + _buildFormatButton(context, ImageFormat.textAndImage), + _buildFormatButton(context, ImageFormat.image), + ], + if (showImageFormats && showTextFormats) _buildDivider(), + if (showTextFormats) ...[ _buildTextFormatButton(context, TextFormat.paragraph), _buildTextFormatButton(context, TextFormat.bulletList), _buildTextFormatButton(context, TextFormat.numberedList), From 75dd5c1d93dda1d26a3d26d36d4a3d103274d67c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:18:36 +0800 Subject: [PATCH 111/384] chore: regenerate --- .../message/ai_message_action_bar.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 48a8459598..6b1d428d04 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -260,8 +260,11 @@ class _ChangeFormatButtonState extends State { constraints: const BoxConstraints(), onClose: () => widget.onOverrideVisibility?.call(false), child: buildButton(context), - popupBuilder: (_) => _ChangeFormatPopoverContent( - onRegenerate: widget.onRegenerate, + popupBuilder: (_) => BlocProvider.value( + value: context.read(), + child: _ChangeFormatPopoverContent( + onRegenerate: widget.onRegenerate, + ), ), ); } @@ -359,11 +362,16 @@ class _ChangeFormatPopoverContentState child: Row( mainAxisSize: MainAxisSize.min, children: [ - ChangeFormatBar( - spacing: 2.0, - predefinedFormat: predefinedFormat, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); + BlocBuilder( + builder: (context, state) { + return ChangeFormatBar( + spacing: 2.0, + showImageFormats: state.aiType.isCloud, + predefinedFormat: predefinedFormat, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); + }, + ); }, ), const HSpace(4.0), From 654e18aacfba7691e74b1d29087fd7995d649238 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 11 Mar 2025 23:13:24 +0800 Subject: [PATCH 112/384] chore: remove local ai --- .../settings/plan/settings_plan_bloc.dart | 2 +- .../setting_ai_view/settings_ai_view.dart | 86 +------------------ .../settings/pages/settings_billing_view.dart | 22 ----- .../settings/pages/settings_plan_view.dart | 57 ------------ frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++----- frontend/rust-lib/Cargo.lock | 5 +- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-ai/src/local_ai/controller.rs | 3 +- 8 files changed, 32 insertions(+), 193 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart index f7512a834e..dcc5156937 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart @@ -148,7 +148,7 @@ class SettingsPlanBloc extends Bloc { usage.freeze(); final newUsage = usage.rebuild((value) { - if (!newInfo.hasAIMax && !newInfo.hasAIOnDevice) { + if (!newInfo.hasAIMax) { value.aiResponsesUnlimited = false; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 7e795306fb..dab07773d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; @@ -10,9 +9,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflow import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -148,23 +145,7 @@ class _LocalAIOnBoarding extends StatelessWidget { )..add(const LocalAIOnBoardingEvent.started()), child: BlocBuilder( builder: (context, state) { - // Show the local AI settings if the user has purchased the AI Local plan - if (state.isPurchaseAILocal) { - return const LocalAISetting(); - } else if (currentWorkspaceMemberRole?.isOwner ?? false) { - // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan - return _UpgradeToAILocalPlan( - onTap: () { - context.read().add( - const LocalAIOnBoardingEvent.addSubscription( - SubscriptionPlanPB.AiLocal, - ), - ); - }, - ); - } else { - return const _AskOwnerUpgradeToLocalAI(); - } + return const LocalAISetting(); }, ), ); @@ -175,68 +156,3 @@ class _LocalAIOnBoarding extends StatelessWidget { } } } - -class _AskOwnerUpgradeToLocalAI extends StatelessWidget { - const _AskOwnerUpgradeToLocalAI(); - - @override - Widget build(BuildContext context) { - return FlowyText( - LocaleKeys.sideBar_askOwnerToUpgradeToLocalAI.tr(), - color: AFThemeExtension.of(context).strongText, - ); - } -} - -class _UpgradeToAILocalPlan extends StatefulWidget { - const _UpgradeToAILocalPlan({required this.onTap}); - - final VoidCallback onTap; - - @override - State<_UpgradeToAILocalPlan> createState() => _UpgradeToAILocalPlanState(); -} - -class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> { - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.sideBar_upgradeToAILocal.tr(), - maxLines: 10, - lineHeight: 1.5, - ), - const VSpace(4), - Opacity( - opacity: 0.6, - child: FlowyText( - LocaleKeys.sideBar_upgradeToAILocalDesc.tr(), - fontSize: 12, - maxLines: 10, - lineHeight: 1.5, - ), - ), - ], - ), - ), - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const CircularProgressIndicator.adaptive(); - } else { - return Toggle( - value: false, - onChanged: (_) => widget.onTap(), - ); - } - }, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart index 450f2167de..77c1116319 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/shared/loading.dart'; import 'package:appflowy/util/int64_extension.dart'; @@ -211,26 +209,6 @@ class _SettingsBillingViewState extends State { ), ), const SettingsDashedDivider(), - - // Currently, the AI Local tile is only available on macOS - // TODO(nathan): enable windows and linux - if (Platform.isMacOS) - _AITile( - plan: SubscriptionPlanPB.AiLocal, - label: LocaleKeys - .settings_billingPage_addons_aiOnDevice_label - .tr(), - description: LocaleKeys - .settings_billingPage_addons_aiOnDevice_description, - activeDescription: LocaleKeys - .settings_billingPage_addons_aiOnDevice_activeDescription, - canceledDescription: LocaleKeys - .settings_billingPage_addons_aiOnDevice_canceledDescription, - subscriptionInfo: - state.subscriptionInfo.addOns.firstWhereOrNull( - (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal, - ), - ), ], ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index bf1e479226..2606a42ab1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -134,46 +134,6 @@ class _SettingsPlanViewState extends State { ), ), const HSpace(8), - - // Currently, the AI Local tile is only available on macOS - // TODO(nathan): enable windows and linux - if (Platform.isMacOS) - Flexible( - child: _AddOnBox( - title: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_title - .tr(), - description: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_description - .tr(), - price: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_price - .tr( - args: [ - SubscriptionPlanPB.AiLocal.priceAnnualBilling, - ], - ), - priceInfo: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_priceInfo - .tr(), - recommend: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_recommend - .tr( - args: [ - SubscriptionPlanPB.AiLocal.priceMonthBilling, - ], - ), - buttonText: state.subscriptionInfo.hasAIOnDevice - ? LocaleKeys - .settings_planPage_planUsage_addons_activeLabel - .tr() - : LocaleKeys - .settings_planPage_planUsage_addons_addLabel - .tr(), - isActive: state.subscriptionInfo.hasAIOnDevice, - plan: SubscriptionPlanPB.AiLocal, - ), - ), ], ), ], @@ -438,23 +398,6 @@ class _PlanUsageSummary extends StatelessWidget { }, ), ], - if (!subscriptionInfo.hasAIOnDevice) ...[ - _ToggleMore( - value: false, - label: LocaleKeys.settings_planPage_planUsage_aiOnDeviceToggle - .tr(), - badgeLabel: - LocaleKeys.settings_planPage_planUsage_aiOnDeviceBadge.tr(), - onTap: () async { - context.read().add( - const SettingsPlanEvent.addSubscription( - SubscriptionPlanPB.AiLocal, - ), - ); - await Future.delayed(const Duration(seconds: 2), () {}); - }, - ), - ], ], ), ], diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index efa77e3c86..31caa5ce69 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a76364694d696767488c8b12e210eefa58453c89#a76364694d696767488c8b12e210eefa58453c89" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e#8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a76364694d696767488c8b12e210eefa58453c89#a76364694d696767488c8b12e210eefa58453c89" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e#8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" dependencies = [ "anyhow", "cfg-if", @@ -232,6 +232,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "winreg 0.55.0", "xattr", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 89ffbc938e..662703340b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a76364694d696767488c8b12e210eefa58453c89" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a76364694d696767488c8b12e210eefa58453c89" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index dd296158e4..730888d7c5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -431,9 +431,10 @@ impl LocalAIController { } let _ = rx.await; } else { - if let Err(err) = self.ai_plugin.destroy_chat_plugin().await { + if let Err(err) = self.ai_plugin.destroy_plugin().await { error!("[AI Plugin] failed to destroy plugin: {:?}", err); } + chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::UpdateLocalAIState, From 20d64cc7aea7c5d29f3a4ca888f13d7091f78239 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 12 Mar 2025 00:09:20 +0800 Subject: [PATCH 113/384] chore: lint --- .../presentation/settings/pages/settings_plan_view.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 2606a42ab1..21896ead0e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/colors.dart'; From cbdac7102564decdec1dc846477eaa8586d21510 Mon Sep 17 00:00:00 2001 From: khorshuheng Date: Wed, 12 Mar 2025 10:21:21 +0800 Subject: [PATCH 114/384] fix: prevent segfault due to infinite recursion in trash view --- frontend/rust-lib/flowy-folder/src/manager.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index b9e41f2998..e2ec405884 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -2074,16 +2074,11 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) /// Get all the child views belong to the view id, including the child views of the child views. fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec { - let child_view_ids = folder - .get_views_belong_to(view_id) - .into_iter() + folder + .get_view_recursively(view_id) + .iter() .map(|view| view.id.clone()) - .collect::>(); - let mut all_child_view_ids = child_view_ids.clone(); - for child_view_id in child_view_ids { - all_child_view_ids.extend(get_all_child_view_ids(folder, &child_view_id)); - } - all_child_view_ids + .collect() } /// Get the current private views of the user. From e553627ee53a5dd67f2cad78ef4679f0e0e52824 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 12 Mar 2025 11:16:51 +0800 Subject: [PATCH 115/384] chore: fix ios build --- .../pages/setting_ai_view/settings_ai_view.dart | 17 +++++++---------- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 15 ++++++++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index dab07773d1..14ecfb3708 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -55,16 +55,13 @@ class SettingsAIView extends StatelessWidget { ]; children.add(const _AISearchToggle(value: false)); - - if (state.currentWorkspaceMemberRole != null) { - children.add( - _LocalAIOnBoarding( - userProfile: userProfile, - currentWorkspaceMemberRole: state.currentWorkspaceMemberRole!, - workspaceId: workspaceId, - ), - ); - } + children.add( + _LocalAIOnBoarding( + userProfile: userProfile, + currentWorkspaceMemberRole: state.currentWorkspaceMemberRole!, + workspaceId: workspaceId, + ), + ); return SettingsBody( title: LocaleKeys.settings_aiPage_title.tr(), diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index a5707bab72..e2a11741c8 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -79,15 +79,20 @@ pub(crate) fn install_path() -> Option { return None; } -#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub(crate) fn offline_app_path() -> PathBuf { - PathBuf::new() -} - pub fn is_plugin_ready() -> bool { ollama_plugin_path().exists() || ollama_plugin_command_available() } +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +pub(crate) fn is_plugin_ready() -> bool { + false +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +pub(crate) fn ollama_plugin_path() -> PathBuf { + PathBuf::new() +} + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { #[cfg(target_os = "windows")] From 1e81e4c68f033dea14a1af22ddfe21e8ce07b937 Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Wed, 12 Mar 2025 06:32:24 +0100 Subject: [PATCH 116/384] chore: update arabic translation Arabic Translation Updates --- frontend/resources/translations/ar-SA.json | 48 ++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 23b9d4abdc..fddf5520db 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -228,6 +228,7 @@ "indexingFile": "الفهرسة {}", "generatingResponse": "توليد الاستجابة", "selectSources": "اختر المصادر", + "currentPage": "الصفحة الحالية", "sourcesLimitReached": "يمكنك فقط تحديد ما يصل إلى 3 مستندات من المستوى العلوي ومستنداتها الفرعية", "sourceUnsupported": "نحن لا ندعم الدردشة مع قواعد البيانات في الوقت الحالي", "regenerate": "حاول ثانية", @@ -763,6 +764,7 @@ "alignLeft": "محاذاة النص إلى اليسار", "alignCenter": "محاذاة النص إلى الوسط", "alignRight": "محاذاة النص إلى اليمين", + "insertInlineMathEquation": "إدراج معادلة رياضية مضمنة", "undo": "التراجع", "redo": "الإعادة", "convertToParagraph": "تحويل الكتلة إلى فقرة", @@ -853,6 +855,8 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", + "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", + "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", "restartLocalAI": "إعادة تشغيل الذكاء الاصطناعي المحلي", "disableLocalAITitle": "تعطيل الذكاء الاصطناعي المحلي", @@ -866,7 +870,11 @@ "offlineAIDownload3": "إنه أولا", "activeOfflineAI": "نشط", "downloadOfflineAI": "التنزيل", - "openModelDirectory": "افتح المجلد" + "openModelDirectory": "افتح المجلد", + "localAISetupInstruction1": "اتبع هؤلاء", + "localAISetupInstruction2": "التعليمات", + "localAISetupInstruction3": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", + "startLocalAI": "قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي" } }, "planPage": { @@ -1411,6 +1419,7 @@ "filterBy": "مصنف بواسطة...", "typeAValue": "اكتب قيمة ...", "layout": "تَخطِيط", + "compactMode": "الوضع المضغوط", "databaseLayout": "تَخطِيط", "viewList": { "zero": "0 مشاهدات", @@ -1758,7 +1767,10 @@ "aiWriter": "كاتب الذكاء الاصطناعي", "dateOrReminder": "التاريخ أو التذكير", "photoGallery": "معرض الصور", - "file": "الملف" + "file": "الملف", + "twoColumns": "عمودين", + "threeColumns": "3 أعمدة", + "fourColumns": "4 أعمدة" }, "subPage": { "name": "المستند", @@ -1781,6 +1793,16 @@ "referencedGrid": "الشبكة المشار إليها", "referencedCalendar": "التقويم المشار إليه", "referencedDocument": "الوثيقة المشار إليها", + "aiWriter": { + "userQuestion": "اسأل الذكاء الاصطناعي عن أي شيء", + "continueWriting": "استمر في الكتابة", + "fixSpelling": "تصحيح الأخطاء الإملائية والنحوية", + "improveWriting": "تحسين الكتابة", + "summarize": "تلخيص", + "explain": "اشرح", + "makeShorter": "اجعلها أقصر", + "makeLonger": "اجعلها أطول" + }, "autoGeneratorMenuItemName": "كاتب AI", "autoGeneratorTitleName": "AI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...", "autoGeneratorLearnMore": "يتعلم أكثر", @@ -2561,6 +2583,7 @@ "accountLogin": "تسجيل الدخول إلى الحساب", "updateNameError": "فشل في تحديث الاسم", "updateIconError": "فشل في تحديث الأيقونة", + "aboutAppFlowy": "حول appName", "deleteAccount": { "title": "حذف الحساب", "subtitle": "احذف حسابك وجميع بياناتك بشكل دائم.", @@ -3130,6 +3153,25 @@ } }, "ai": { - "contentPolicyViolation": "فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى" + "contentPolicyViolation": "فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى", + "textLimitReachedDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة للذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", + "imageLimitReachedDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يُرجى الترقية إلى الخطة الاحترافية أو شراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", + "limitReachedAction": { + "textDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. للحصول على المزيد من الاستجابات، يرجى", + "imageDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يرجى", + "upgrade": "ترقية", + "toThe": "الى", + "proPlan": "الخطة الاحترافية", + "orPurchaseAn": "أو شراء", + "aiAddon": "مَرافِق الذكاء الاصطناعي" + }, + "editing": "تحرير", + "analyzing": "تحليل", + "continueWritingEmptyDocumentTitle": "استمر في كتابة الخطأ", + "continueWritingEmptyDocumentDescription": "نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!" + }, + "autoUpdate": { + "criticalUpdateTitle": "التحديث ضروري للمتابعة", + "criticalUpdateDescription": "لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق." } } From 070cde9ecb6d11693b75609fe3627558d917b95d Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 12 Mar 2025 13:34:59 +0800 Subject: [PATCH 117/384] chore: bump version 0.8.7 (#7512) --- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 89ee36ad6a..44337645fe 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.6" +APPFLOWY_VERSION = "0.8.7" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 71b603bdaa..4d185d1fca 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.6 +version: 0.8.7 environment: flutter: ">=3.27.4" From b59eba76a65c476ab9e24d214cbc5fe37f6cc8f9 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:54:00 +0800 Subject: [PATCH 118/384] fix: hide continue writing when document is empty (#7498) * fix: hide continue writing when document is empty * chore: code clean up and add documentation --- .../database/widgets/row/row_document.dart | 14 +++- .../database_document_page.dart | 6 ++ .../lib/plugins/document/document_page.dart | 4 + .../document/presentation/editor_page.dart | 3 + .../ai/ai_writer_block_component.dart | 80 ++++++++++++++----- .../operations/ai_writer_node_extension.dart | 62 ++++++++++++++ .../slash_menu/slash_menu_items_builder.dart | 13 ++- 7 files changed, 156 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart index 0489db8907..8e229f6b21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_con import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -71,9 +72,16 @@ class _RowEditor extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - DocumentBloc(documentId: view.id)..add(const DocumentEvent.initial()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => DocumentBloc(documentId: view.id) + ..add(const DocumentEvent.initial()), + ), + BlocProvider( + create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), + ), + ], child: BlocConsumer( listenWhen: (previous, current) => previous.isDocumentEmpty != current.isDocumentEmpty, diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart index eaa82b22e9..186671b427 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart @@ -21,6 +21,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../workspace/application/view/view_bloc.dart'; + // This widget is largely copied from `plugins/document/document_page.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. class DatabaseDocumentPage extends StatefulWidget { @@ -72,6 +74,10 @@ class _DatabaseDocumentPageState extends State { documentId: widget.documentId, )..add(const DocumentEvent.initial()), ), + BlocProvider( + create: (_) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + ), ], child: BlocBuilder( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 5c695fa508..4e15128788 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -92,6 +92,10 @@ class _DocumentPageState extends State ViewLockStatusEvent.initial(), ), ), + BlocProvider( + create: (_) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + ), ], child: BlocConsumer( listenWhen: (prev, curr) => curr.isLocked != prev.isLocked, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index b5f32d9f3c..9c5302bcd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -15,6 +15,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; @@ -437,11 +438,13 @@ class _AppFlowyEditorPageState extends State }) { final documentBloc = context.read(); final isLocalMode = documentBloc.isLocalMode; + final view = context.read().state.view; return slashMenuItemsBuilder( editorState: editorState, node: node, isLocalMode: isLocalMode, documentBloc: documentBloc, + view: view, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index ee6cf16909..ed77fadae4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -187,6 +188,7 @@ class _AIWriterBlockComponentState extends State { ), width: constraints.maxWidth, child: OverlayContent( + editorState: editorState, node: widget.node, ), ), @@ -236,9 +238,11 @@ class _AIWriterBlockComponentState extends State { class OverlayContent extends StatelessWidget { const OverlayContent({ super.key, + required this.editorState, required this.node, }); + final EditorState editorState; final Node node; @override @@ -372,28 +376,12 @@ class OverlayContent extends StatelessWidget { ], ), ), - if (showActionPopup) ...[ - const VSpace(4.0 + 1.0), - Container( - padding: EdgeInsets.all(8.0), - constraints: BoxConstraints(minWidth: 240.0), - decoration: _getModalDecoration( - context, - color: Theme.of(context).colorScheme.surface, - borderColor: lightBorderColor, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - child: IntrinsicWidth( - child: SeparatedColumn( - separatorBuilder: () => const VSpace(4.0), - crossAxisAlignment: CrossAxisAlignment.start, - children: _getCommands( - hasSelection: hasSelection, - ), - ), - ), - ), - ], + ..._bottomActions( + context, + showActionPopup, + hasSelection, + lightBorderColor, + ), ], ); }, @@ -477,6 +465,54 @@ class OverlayContent extends StatelessWidget { ); } + List _bottomActions( + BuildContext context, + bool showActionPopup, + bool hasSelection, + Color borderColor, + ) { + if (!showActionPopup) { + return []; + } + + if (editorState.isEmptyForContinueWriting()) { + final documentContext = editorState.document.root.context; + if (documentContext == null) { + return []; + } + final view = documentContext.read().state.view; + if (view.name.isEmpty) { + return []; + } + } + + return [ + // add one here to take into account the border of the main message box. + // It is configured to be on the outside to hide some graphical + // artifacts. + const VSpace(4.0 + 1.0), + Container( + padding: EdgeInsets.all(8.0), + constraints: BoxConstraints(minWidth: 240.0), + decoration: _getModalDecoration( + context, + color: Theme.of(context).colorScheme.surface, + borderColor: borderColor, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + child: IntrinsicWidth( + child: SeparatedColumn( + separatorBuilder: () => const VSpace(4.0), + crossAxisAlignment: CrossAxisAlignment.start, + children: _getCommands( + hasSelection: hasSelection, + ), + ), + ), + ), + ]; + } + List _getCommands({required bool hasSelection}) { if (hasSelection) { return [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 1335f51df5..1119917e62 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -87,4 +89,64 @@ extension AiWriterNodeExtension on EditorState { return res; } + + /// Determines whether the document is empty up to the selection + /// + /// If empty and the title is also empty, the continue writing option will be disabled. + bool isEmptyForContinueWriting({ + Selection? selection, + }) { + if (selection != null && !selection.isCollapsed) { + return false; + } + + final effectiveSelection = Selection( + start: Position(path: [0]), + end: selection?.normalized.end ?? + this.selection?.normalized.end ?? + Position(path: [0]), + ); + + // if the selected nodes are not entirely selected, slice the nodes + final slicedNodes = []; + final nodes = getNodesInSelection(effectiveSelection); + + for (final node in nodes) { + final delta = node.delta; + if (delta == null) { + continue; + } + + final slicedDelta = delta.slice( + node == nodes.first ? effectiveSelection.startIndex : 0, + node == nodes.last ? effectiveSelection.endIndex : delta.length, + ); + + final copiedNode = node.copyWith( + attributes: { + ...node.attributes, + blockComponentDelta: slicedDelta.toJson(), + }, + ); + + slicedNodes.add(copiedNode); + } + + // using less custom parsers to avoid futures + final markdown = documentToMarkdown( + Document.blank()..insert([0], slicedNodes), + customParsers: [ + const MathEquationNodeParser(), + const CalloutNodeParser(), + const ToggleListNodeParser(), + const CustomParagraphNodeParser(), + const SubPageNodeParser(), + const SimpleTableNodeParser(), + const LinkPreviewNodeParser(), + const FileBlockNodeParser(), + ], + ); + + return markdown.trim().isEmpty; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart index e953694966..137f592902 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart @@ -1,5 +1,7 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -13,9 +15,16 @@ List slashMenuItemsBuilder({ DocumentBloc? documentBloc, EditorState? editorState, Node? node, + ViewPB? view, }) { final isInTable = node != null && node.parentTableCellNode != null; final isMobile = UniversalPlatform.isMobile; + bool isEmpty = false; + if (editorState == null || editorState.isEmptyForContinueWriting()) { + if (view == null || view.name.isEmpty) { + isEmpty = true; + } + } if (isMobile) { if (isInTable) { return mobileItemsInTale; @@ -29,6 +38,7 @@ List slashMenuItemsBuilder({ return _defaultSlashMenuItems( isLocalMode: isLocalMode, documentBloc: documentBloc, + isEmpty: isEmpty, ); } } @@ -48,11 +58,12 @@ List slashMenuItemsBuilder({ List _defaultSlashMenuItems({ bool isLocalMode = false, DocumentBloc? documentBloc, + bool isEmpty = false, }) { return [ // ai if (!isLocalMode) ...[ - continueWritingSlashMenuItem, + if (!isEmpty) continueWritingSlashMenuItem, aiWriterSlashMenuItem, ], From 8d50caa86ef1b0f3768871405ecf6ad19fc5d677 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 12 Mar 2025 13:54:24 +0800 Subject: [PATCH 119/384] fix: remove layout builder in quote block (#7508) * fix: remove layout builder in quote block * fix: quote block selection color * fix: quote block and callout block background color issue * fix: background color in callout block * fix: quote block layout on mobile --- .../presentation/editor_configuration.dart | 15 ++-- .../callout/callout_block_component.dart | 36 +++++++--- .../simple_columns_block_component.dart | 14 ++-- .../quote/quote_block_component.dart | 68 +++++++++++-------- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++------- 5 files changed, 106 insertions(+), 73 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 653bebf902..c22cbb648c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -124,14 +124,14 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) { double padding = 26.0; // only add indent padding for the top level node to align the children - if (UniversalPlatform.isMobile && node.path.length == 1) { - padding += EditorStyleCustomizer.nodeHorizontalPadding; + if (UniversalPlatform.isMobile && node.level == 1) { + padding += EditorStyleCustomizer.nodeHorizontalPadding - 4; } // in the quote block, we reduce the indent padding for the first level block. // So we have to add more padding for the second level to avoid the drag menu overlay the quote icon. - if (node.isInQuote && node.level == 2) { - padding += 24; + if (node.isInQuote && node.level == 2 && UniversalPlatform.isDesktop) { + padding += 22; } return textDirection == TextDirection.ltr @@ -611,7 +611,12 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( node: node, configuration: configuration, ), - indentPadding: (node, _) => EdgeInsets.zero, + indentPadding: (node, textDirection) { + if (UniversalPlatform.isMobile) { + return configuration.indentPadding(node, textDirection); + } + return EdgeInsets.zero; + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 9aa97a20b2..1e8d981c15 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -187,10 +187,10 @@ class _CalloutBlockComponentWidgetState @override Widget buildComponentWithChildren(BuildContext context) { - return Stack( + Widget child = Stack( children: [ Positioned.fill( - left: cachedLeft, + left: UniversalPlatform.isMobile ? 0 : cachedLeft, child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(6.0)), @@ -208,6 +208,15 @@ class _CalloutBlockComponentWidgetState ), ], ); + + if (UniversalPlatform.isMobile) { + child = Padding( + padding: padding, + child: child, + ); + } + + return child; } // build the callout block widget @@ -224,16 +233,17 @@ class _CalloutBlockComponentWidgetState Widget child = Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(6.0)), - color: backgroundColor, + color: withBackgroundColor ? backgroundColor : null, ), padding: widget.inlinePadding(widget.node), width: double.infinity, alignment: alignment, child: Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, textDirection: textDirection, children: [ - if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), + const HSpace(6.0), // the emoji picker button for the note EmojiPickerButton( // force to refresh the popover state @@ -270,11 +280,19 @@ class _CalloutBlockComponentWidgetState ), ); - child = Padding( - key: blockComponentKey, - padding: EdgeInsets.zero, - child: child, - ); + if (UniversalPlatform.isMobile && node.children.isEmpty) { + child = Padding( + key: blockComponentKey, + padding: padding, + child: child, + ); + } else { + child = Padding( + key: blockComponentKey, + padding: EdgeInsets.zero, + child: child, + ); + } child = BlockSelectionContainer( node: node, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index f5b0b14400..8408c0f775 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -162,12 +162,14 @@ class ColumnsBlockComponentState extends State children.add(child); - children.add( - SimpleColumnBlockWidthResizer( - columnNode: childNode, - editorState: editorState, - ), - ); + if (i != length - 1) { + children.add( + SimpleColumnBlockWidthResizer( + columnNode: childNode, + editorState: editorState, + ), + ); + } } return children; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart index 28729e64a6..211a99c1bf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:universal_platform/universal_platform.dart'; typedef QuoteBlockIconBuilder = Widget Function( BuildContext context, @@ -124,7 +125,7 @@ class _QuoteBlockComponentWidgetState extends State void initState() { super.initState(); - _observerQuoteBlockChanges(); + _updateQuoteBlockHeight(); } @override @@ -136,20 +137,45 @@ class _QuoteBlockComponentWidgetState extends State @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { + return NotificationListener( + key: layoutBuilderKey, + onNotification: (notification) { _updateQuoteBlockHeight(); - - return KeyedSubtree( - key: layoutBuilderKey, - child: node.children.isEmpty - ? buildComponent(context) - : buildComponentWithChildren(context), - ); + return true; }, + child: SizeChangedLayoutNotifier( + child: node.children.isEmpty + ? buildComponent(context) + : buildComponentWithChildren(context), + ), ); } + @override + Widget buildComponentWithChildren(BuildContext context) { + final Widget child = Stack( + children: [ + Positioned.fill( + left: UniversalPlatform.isMobile ? padding.left : cachedLeft, + right: UniversalPlatform.isMobile ? padding.right : 0, + child: Container( + color: backgroundColor, + ), + ), + NestedListWidget( + indentPadding: indentPadding, + child: buildComponent(context, withBackgroundColor: false), + children: editorState.renderer.buildList( + context, + widget.node.children, + ), + ), + ], + ); + + return child; + } + @override Widget buildComponent( BuildContext context, { @@ -204,7 +230,7 @@ class _QuoteBlockComponentWidgetState extends State ); child = Container( - color: backgroundColor, + color: withBackgroundColor ? backgroundColor : null, child: Padding( key: blockComponentKey, padding: padding, @@ -218,6 +244,7 @@ class _QuoteBlockComponentWidgetState extends State listenable: editorState.selectionNotifier, remoteSelection: editorState.remoteSelections, blockColor: editorState.editorStyle.selectionColor, + selectionAboveBlock: true, supportTypes: const [ BlockSelectionType.block, ], @@ -247,24 +274,6 @@ class _QuoteBlockComponentWidgetState extends State } }); } - - void _observerQuoteBlockChanges() { - _transactionSubscription = editorState.transactionStream.listen((event) { - final time = event.$1; - - if (time != TransactionTime.before) { - return; - } - - final transaction = event.$2; - final operations = transaction.operations; - for (final operation in operations) { - if (node.path.isAncestorOf(operation.path)) { - _updateQuoteBlockHeight(); - } - } - }); - } } class QuoteIcon extends StatelessWidget { @@ -287,7 +296,6 @@ class QuoteIcon extends StatelessWidget { padding: const EdgeInsets.only(right: 6.0), child: SizedBox( width: 3 * textScaleFactor, - // use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote child: OverflowBox( alignment: Alignment.topCenter, diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 From 44945b2912fcdcb52246ccdd7ff0d5f1ed56a167 Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 12 Mar 2025 14:10:41 +0800 Subject: [PATCH 120/384] fix: some slash menu issues (#7501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: slash menu unexpectedly overflows the screen * fix: the style of no result doesn’t match the design * fix: unexpected flashing effect on 2nd level menu item * fix: can not back to last level through backspace --- .../selection_menu/mobile_selection_menu.dart | 9 +++-- .../mobile_selection_menu_item_widget.dart | 4 +-- .../mobile_selection_menu_widget.dart | 35 ++++++++++++++----- frontend/appflowy_flutter/pubspec.lock | 4 +-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart index b65c9f9347..07eceac51a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart @@ -186,8 +186,9 @@ class MobileSelectionMenu extends SelectionMenuService { final bottomRight = rect.bottomRight; final topRight = rect.topRight; var offset = bottomRight + menuOffset; + final limitX = editorWidth - menuWidth + editorOffset.dx; _offset = Offset( - offset.dx, + min(offset.dx, limitX), offset.dy, ); @@ -196,10 +197,9 @@ class MobileSelectionMenu extends SelectionMenuService { offset = topRight - menuOffset; _alignment = Alignment.bottomLeft; - final limitX = editorWidth - menuWidth; _offset = Offset( - min(offset.dx, limitX), - MediaQuery.of(context).size.height - offset.dy, + offset.dx, + editorOffset.dy + editorHeight - offset.dy, ); } @@ -210,7 +210,6 @@ class MobileSelectionMenu extends SelectionMenuService { : Alignment.bottomRight; final x = editorWidth - _offset.dx + editorOffset.dx; - final limitX = editorWidth - menuWidth + editorOffset.dx; _offset = Offset( min(x, limitX), _offset.dy, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart index ae5b0b11ac..bdee8f1857 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart @@ -36,9 +36,7 @@ class MobileSelectionMenuItemWidget extends StatelessWidget { ), style: ButtonStyle( alignment: Alignment.centerLeft, - overlayColor: WidgetStateProperty.all( - style.selectionMenuItemSelectedColor, - ), + overlayColor: WidgetStateProperty.all(Colors.transparent), backgroundColor: isSelected ? WidgetStateProperty.all( style.selectionMenuItemSelectedColor, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart index ed080e6186..e259d49d52 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart @@ -1,6 +1,8 @@ import 'dart:math'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'mobile_selection_menu_item.dart'; @@ -314,15 +316,32 @@ class _MobileSelectionMenuWidgetState extends State { } Widget _buildNoResultsWidget(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withValues(alpha: 0.1), + ), + ], + borderRadius: BorderRadius.circular(12.0), + ), child: SizedBox( - width: 140, - child: Material( - child: Text( - "No results", - style: TextStyle(fontSize: 18.0, color: Colors.grey), - textAlign: TextAlign.center, + width: 240, + height: 48, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Material( + color: Colors.transparent, + child: Center( + child: Text( + LocaleKeys.inlineActions_noResults.tr(), + style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)), + textAlign: TextAlign.center, + ), + ), ), ), ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 704ada89e6..7537db010a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5841b4c" - resolved-ref: "5841b4c1bab170da66c52c60977690f2817b8fc4" + ref: "8db5bdf" + resolved-ref: "8db5bdff8f2f6258800660123452ddbde1c14059" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 4d185d1fca..c537953906 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "5841b4c" + ref: "8db5bdf" appflowy_editor_plugins: git: From 1f7ab9d22d93cc3582e1d302d94defb851b58c9a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 12 Mar 2025 15:08:37 +0800 Subject: [PATCH 121/384] chore: fix ios compile --- frontend/rust-lib/flowy-ai/src/local_ai/watch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index e2a11741c8..da6c30a69a 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -79,12 +79,13 @@ pub(crate) fn install_path() -> Option { return None; } +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] pub fn is_plugin_ready() -> bool { ollama_plugin_path().exists() || ollama_plugin_command_available() } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub(crate) fn is_plugin_ready() -> bool { +pub fn is_plugin_ready() -> bool { false } From d15a8a88a6a06a2249dfbb853d08b6bc136524f0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 12 Mar 2025 20:29:03 +0800 Subject: [PATCH 122/384] chore: disable input when local ai is initializing --- .../lib/ai/service/ai_prompt_input_bloc.dart | 26 +- .../desktop_prompt_text_field.dart | 13 +- .../application/chat_ai_message_bloc.dart | 250 +++++++++--------- .../application/chat_message_stream.dart | 121 ++++++--- .../lib/plugins/ai_chat/chat_page.dart | 3 + .../presentation/message/ai_text_message.dart | 15 ++ .../ai/local_ai_setting_panel_bloc.dart | 4 +- .../settings/ai/local_llm_listener.dart | 4 +- .../settings/ai/plugin_state_bloc.dart | 4 +- .../setting_ai_view/local_ai_setting.dart | 105 ++++---- .../setting_ai_view/settings_ai_view.dart | 8 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++-- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 4 + .../flowy-ai/src/local_ai/controller.rs | 6 +- .../src/middleware/chat_service_mw.rs | 28 +- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 5 + 19 files changed, 364 insertions(+), 289 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 9d42370ed4..7e05bffca9 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -7,6 +8,7 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -17,14 +19,14 @@ part 'ai_prompt_input_bloc.freezed.dart'; class AIPromptInputBloc extends Bloc { AIPromptInputBloc({ required PredefinedFormat? predefinedFormat, - }) : _listener = LocalLLMListener(), + }) : _listener = LocalAIStateListener(), super(AIPromptInputState.initial(predefinedFormat)) { _dispatch(); _startListening(); _init(); } - final LocalLLMListener _listener; + final LocalAIStateListener _listener; @override Future close() async { @@ -41,16 +43,32 @@ class AIPromptInputBloc extends Bloc { bool supportChatWithFile = aiType.isLocal && localAIState.state == RunningStatePB.Running; + // If local ai is enabled, user can only send messages when the AI is running + final editable = localAIState.enabled + ? localAIState.state == RunningStatePB.Running + : true; + if (localAIState.hasLackOfResource()) { aiType = AiType.cloud; supportChatWithFile = false; } + var hintText = aiType.isLocal + ? LocaleKeys.chat_inputLocalAIMessageHint.tr() + : LocaleKeys.chat_inputMessageHint.tr(); + + if (editable == false && aiType.isLocal) { + hintText = + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); + } + emit( state.copyWith( aiType: aiType, supportChatWithFile: supportChatWithFile, localAIState: localAIState, + editable: editable, + hintText: hintText, ), ); }, @@ -179,6 +197,8 @@ class AIPromptInputState with _$AIPromptInputState { required LocalAIPB? localAIState, required List attachedFiles, required List mentionedPages, + required bool editable, + required String hintText, }) = _AIPromptInputState; factory AIPromptInputState.initial(PredefinedFormat? format) => @@ -190,6 +210,8 @@ class AIPromptInputState with _$AIPromptInputState { localAIState: null, attachedFiles: [], mentionedPages: [], + editable: true, + hintText: '', ); } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index b52898880c..17fd35ec9b 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -1,11 +1,9 @@ import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -52,7 +50,6 @@ class _DesktopPromptInputState extends State { super.initState(); textController.addListener(handleTextControllerChanged); - focusNode.addListener( () { if (!widget.hideDecoration) { @@ -377,15 +374,13 @@ class _DesktopPromptInputState extends State { builder: (context, state) { return PromptInputTextField( key: textFieldKey, + editable: state.editable, cubit: inputControlCubit, textController: textController, textFieldFocusNode: focusNode, contentPadding: calculateContentPadding(state.showPredefinedFormats), - hintText: switch (state.aiType) { - AiType.cloud => LocaleKeys.chat_inputMessageHint.tr(), - AiType.local => LocaleKeys.chat_inputLocalAIMessageHint.tr() - }, + hintText: state.hintText, ); }, ), @@ -491,6 +486,7 @@ class _FocusNextItemIntent extends Intent { class PromptInputTextField extends StatelessWidget { const PromptInputTextField({ super.key, + required this.editable, required this.cubit, required this.textController, required this.textFieldFocusNode, @@ -502,6 +498,7 @@ class PromptInputTextField extends StatelessWidget { final TextEditingController textController; final FocusNode textFieldFocusNode; final EdgeInsetsGeometry contentPadding; + final bool editable; final String hintText; @override @@ -509,6 +506,8 @@ class PromptInputTextField extends StatelessWidget { return ExtendedTextField( controller: textController, focusNode: textFieldFocusNode, + readOnly: !editable, + enabled: editable, decoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index 60fca000c0..47c1668a2c 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -23,11 +23,126 @@ class ChatAIMessageBloc extends Bloc { parseMetadata(refSourceJsonString), ), ) { - _dispatch(); + _registerEventHandlers(); + _initializeStreamListener(); + _checkInitialStreamState(); + } + final String chatId; + final Int64? questionId; + + void _registerEventHandlers() { + on<_UpdateText>((event, emit) { + emit( + state.copyWith( + text: event.text, + messageState: const MessageState.ready(), + ), + ); + }); + + on<_ReceiveError>((event, emit) { + emit(state.copyWith(messageState: MessageState.onError(event.error))); + }); + + on<_Retry>((event, emit) async { + if (questionId == null) { + Log.error("Question id is not valid: $questionId"); + return; + } + emit(state.copyWith(messageState: const MessageState.loading())); + final payload = ChatMessageIdPB( + chatId: chatId, + messageId: questionId, + ); + final result = await AIEventGetAnswerForQuestion(payload).send(); + if (!isClosed) { + result.fold( + (answer) => add(ChatAIMessageEvent.retryResult(answer.content)), + (err) { + Log.error("Failed to get answer: $err"); + add(ChatAIMessageEvent.receiveError(err.toString())); + }, + ); + } + }); + + on<_RetryResult>((event, emit) { + emit( + state.copyWith( + text: event.text, + messageState: const MessageState.ready(), + ), + ); + }); + + on<_OnAIResponseLimit>((event, emit) { + emit( + state.copyWith( + messageState: const MessageState.onAIResponseLimit(), + ), + ); + }); + + on<_OnAIImageResponseLimit>((event, emit) { + emit( + state.copyWith( + messageState: const MessageState.onAIImageResponseLimit(), + ), + ); + }); + + on<_OnAIMaxRquired>((event, emit) { + emit( + state.copyWith( + messageState: MessageState.onAIMaxRequired(event.message), + ), + ); + }); + + on<_OnLocalAIInitializing>((event, emit) { + emit( + state.copyWith( + messageState: const MessageState.onInitializingLocalAI(), + ), + ); + }); + + on<_ReceiveMetadata>((event, emit) { + Log.debug("AI Steps: ${event.metadata.progress?.step}"); + emit( + state.copyWith( + sources: event.metadata.sources, + progress: event.metadata.progress, + ), + ); + }); + } + + void _initializeStreamListener() { if (state.stream != null) { - _startListening(); + state.stream!.listen( + onData: (text) => _safeAdd(ChatAIMessageEvent.updateText(text)), + onError: (error) => + _safeAdd(ChatAIMessageEvent.receiveError(error.toString())), + onAIResponseLimit: () => + _safeAdd(const ChatAIMessageEvent.onAIResponseLimit()), + onAIImageResponseLimit: () => + _safeAdd(const ChatAIMessageEvent.onAIImageResponseLimit()), + onMetadata: (metadata) => + _safeAdd(ChatAIMessageEvent.receiveMetadata(metadata)), + onAIMaxRequired: (message) { + Log.info(message); + _safeAdd(ChatAIMessageEvent.onAIMaxRequired(message)); + }, + onLocalAIInitializing: () => + _safeAdd(const ChatAIMessageEvent.onLocalAIInitializing()), + ); + } + } + void _checkInitialStreamState() { + if (state.stream != null) { if (state.stream!.aiLimitReached) { add(const ChatAIMessageEvent.onAIResponseLimit()); } else if (state.stream!.error != null) { @@ -36,130 +151,10 @@ class ChatAIMessageBloc extends Bloc { } } - final String chatId; - final Int64? questionId; - - void _dispatch() { - on( - (event, emit) { - event.when( - updateText: (newText) { - emit( - state.copyWith( - text: newText, - messageState: const MessageState.ready(), - ), - ); - }, - receiveError: (error) { - emit(state.copyWith(messageState: MessageState.onError(error))); - }, - retry: () { - if (questionId is! Int64) { - Log.error("Question id is not Int64: $questionId"); - return; - } - emit( - state.copyWith( - messageState: const MessageState.loading(), - ), - ); - - final payload = ChatMessageIdPB( - chatId: chatId, - messageId: questionId, - ); - AIEventGetAnswerForQuestion(payload).send().then((result) { - if (!isClosed) { - result.fold( - (answer) { - add(ChatAIMessageEvent.retryResult(answer.content)); - }, - (err) { - Log.error("Failed to get answer: $err"); - add(ChatAIMessageEvent.receiveError(err.toString())); - }, - ); - } - }); - }, - retryResult: (String text) { - emit( - state.copyWith( - text: text, - messageState: const MessageState.ready(), - ), - ); - }, - onAIResponseLimit: () { - emit( - state.copyWith( - messageState: const MessageState.onAIResponseLimit(), - ), - ); - }, - onAIImageResponseLimit: () { - emit( - state.copyWith( - messageState: const MessageState.onAIImageResponseLimit(), - ), - ); - }, - onAIMaxRequired: (message) { - emit( - state.copyWith( - messageState: MessageState.onAIMaxRequired(message), - ), - ); - }, - receiveMetadata: (metadata) { - Log.debug("AI Steps: ${metadata.progress?.step}"); - emit( - state.copyWith( - sources: metadata.sources, - progress: metadata.progress, - ), - ); - }, - ); - }, - ); - } - - void _startListening() { - state.stream!.listen( - onData: (text) { - if (!isClosed) { - add(ChatAIMessageEvent.updateText(text)); - } - }, - onError: (error) { - if (!isClosed) { - add(ChatAIMessageEvent.receiveError(error.toString())); - } - }, - onAIResponseLimit: () { - if (!isClosed) { - add(const ChatAIMessageEvent.onAIResponseLimit()); - } - }, - onAIImageResponseLimit: () { - if (!isClosed) { - add(const ChatAIMessageEvent.onAIImageResponseLimit()); - } - }, - onMetadata: (metadata) { - if (!isClosed) { - add(ChatAIMessageEvent.receiveMetadata(metadata)); - } - }, - onAIMaxRequired: (message) { - if (!isClosed) { - Log.info(message); - add(ChatAIMessageEvent.onAIMaxRequired(message)); - } - }, - ); + void _safeAdd(ChatAIMessageEvent event) { + if (!isClosed) { + add(event); + } } } @@ -174,6 +169,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent { _OnAIImageResponseLimit; const factory ChatAIMessageEvent.onAIMaxRequired(String message) = _OnAIMaxRquired; + const factory ChatAIMessageEvent.onLocalAIInitializing() = + _OnLocalAIInitializing; const factory ChatAIMessageEvent.receiveMetadata( MetadataCollection metadata, ) = _ReceiveMetadata; @@ -209,6 +206,7 @@ class MessageState with _$MessageState { const factory MessageState.onAIResponseLimit() = _AIResponseLimit; const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit; const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired; + const factory MessageState.onInitializingLocalAI() = _LocalAIInitializing; const factory MessageState.ready() = _Ready; const factory MessageState.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart index df6c1993a1..00d48e9347 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart @@ -4,53 +4,33 @@ import 'dart:isolate'; import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart'; +/// Constants for event prefixes. +class AnswerEventPrefix { + static const data = 'data:'; + static const error = 'error:'; + static const metadata = 'metadata:'; + static const aiResponseLimit = 'AI_RESPONSE_LIMIT'; + static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT'; + static const aiMaxRequired = 'AI_MAX_REQUIRED:'; + static const localAINotReady = 'LOCAL_AI_NOT_READY'; +} + +/// A stream that receives answer events from an isolate or external process. +/// It caches events that might occur before a listener is attached. class AnswerStream { AnswerStream() { _port.handler = _controller.add; _subscription = _controller.stream.listen( - (event) { - if (event.startsWith("data:")) { - _hasStarted = true; - final newText = event.substring(5); - _text += newText; - _onData?.call(_text); - } else if (event.startsWith("error:")) { - _error = event.substring(5); - _onError?.call(_error!); - } else if (event.startsWith("metadata:")) { - if (_onMetadata != null) { - final s = event.substring(9); - _onMetadata!(parseMetadata(s)); - } - } else if (event == "AI_RESPONSE_LIMIT") { - _aiLimitReached = true; - _onAIResponseLimit?.call(); - } else if (event == "AI_IMAGE_RESPONSE_LIMIT") { - _aiImageLimitReached = true; - _onAIImageResponseLimit?.call(); - } else if (event.startsWith("AI_MAX_REQUIRED:")) { - final msg = event.substring(16); - // If the callback is not registered yet, add the event to the buffer. - if (_onAIMaxRequired != null) { - _onAIMaxRequired!(msg); - } else { - _pendingAIMaxRequiredEvents.add(msg); - } - } - }, - onDone: () { - _onEnd?.call(); - }, - onError: (error) { - _error = error.toString(); - _onError?.call(error.toString()); - }, + _handleEvent, + onDone: _onDoneCallback, + onError: _handleError, ); } final RawReceivePort _port = RawReceivePort(); final StreamController _controller = StreamController.broadcast(); late StreamSubscription _subscription; + bool _hasStarted = false; bool _aiLimitReached = false; bool _aiImageLimitReached = false; @@ -62,13 +42,15 @@ class AnswerStream { void Function()? _onStart; void Function()? _onEnd; void Function(String error)? _onError; + void Function()? _onLocalAIInitializing; void Function()? _onAIResponseLimit; void Function()? _onAIImageResponseLimit; void Function(String message)? _onAIMaxRequired; - void Function(MetadataCollection metadataCollection)? _onMetadata; + void Function(MetadataCollection metadata)? _onMetadata; - // Buffer for events that occur before listen() is called. + // Caches for events that occur before listen() is called. final List _pendingAIMaxRequiredEvents = []; + bool _pendingLocalAINotReady = false; int get nativePort => _port.sendPort.nativePort; bool get hasStarted => _hasStarted; @@ -77,12 +59,61 @@ class AnswerStream { String? get error => _error; String get text => _text; + /// Releases the resources used by the AnswerStream. Future dispose() async { await _controller.close(); await _subscription.cancel(); _port.close(); } + /// Handles incoming events from the underlying stream. + void _handleEvent(String event) { + if (event.startsWith(AnswerEventPrefix.data)) { + _hasStarted = true; + final newText = event.substring(AnswerEventPrefix.data.length); + _text += newText; + _onData?.call(_text); + } else if (event.startsWith(AnswerEventPrefix.error)) { + _error = event.substring(AnswerEventPrefix.error.length); + _onError?.call(_error!); + } else if (event.startsWith(AnswerEventPrefix.metadata)) { + final s = event.substring(AnswerEventPrefix.metadata.length); + _onMetadata?.call(parseMetadata(s)); + } else if (event == AnswerEventPrefix.aiResponseLimit) { + _aiLimitReached = true; + _onAIResponseLimit?.call(); + } else if (event == AnswerEventPrefix.aiImageResponseLimit) { + _aiImageLimitReached = true; + _onAIImageResponseLimit?.call(); + } else if (event.startsWith(AnswerEventPrefix.aiMaxRequired)) { + final msg = event.substring(AnswerEventPrefix.aiMaxRequired.length); + if (_onAIMaxRequired != null) { + _onAIMaxRequired!(msg); + } else { + _pendingAIMaxRequiredEvents.add(msg); + } + } else if (event.startsWith(AnswerEventPrefix.localAINotReady)) { + if (_onLocalAIInitializing != null) { + _onLocalAIInitializing!(); + } else { + _pendingLocalAINotReady = true; + } + } + } + + void _onDoneCallback() { + _onEnd?.call(); + } + + void _handleError(dynamic error) { + _error = error.toString(); + _onError?.call(_error!); + } + + /// Registers listeners for various events. + /// + /// If certain events have already occurred (e.g. AI_MAX_REQUIRED or LOCAL_AI_NOT_READY), + /// they will be flushed immediately. void listen({ void Function(String text)? onData, void Function()? onStart, @@ -92,6 +123,7 @@ class AnswerStream { void Function()? onAIImageResponseLimit, void Function(String message)? onAIMaxRequired, void Function(MetadataCollection metadata)? onMetadata, + void Function()? onLocalAIInitializing, }) { _onData = onData; _onStart = onStart; @@ -99,10 +131,11 @@ class AnswerStream { _onError = onError; _onAIResponseLimit = onAIResponseLimit; _onAIImageResponseLimit = onAIImageResponseLimit; - _onMetadata = onMetadata; _onAIMaxRequired = onAIMaxRequired; + _onMetadata = onMetadata; + _onLocalAIInitializing = onLocalAIInitializing; - // Flush any buffered AI_MAX_REQUIRED events. + // Flush pending AI_MAX_REQUIRED events. if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) { for (final msg in _pendingAIMaxRequiredEvents) { _onAIMaxRequired!(msg); @@ -110,6 +143,12 @@ class AnswerStream { _pendingAIMaxRequiredEvents.clear(); } + // Flush pending LOCAL_AI_NOT_READY event. + if (_pendingLocalAINotReady && _onLocalAIInitializing != null) { + _onLocalAIInitializing!(); + _pendingLocalAINotReady = false; + } + _onStart?.call(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index f7f22a3c93..5221ab3766 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -247,6 +247,9 @@ class _ChatContentPage extends StatelessWidget { onChangeFormat: (format) => context .read() .add(ChatEvent.regenerateAnswer(message.id, format)), + onStopStream: () => context.read().add( + const ChatEvent.stopStream(), + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index d6b3c87903..5a73501583 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -32,6 +32,7 @@ class ChatAIMessageWidget extends StatelessWidget { required this.questionId, required this.chatId, required this.refSourceJsonString, + required this.onStopStream, this.onSelectedMetadata, this.onRegenerate, this.onChangeFormat, @@ -50,6 +51,7 @@ class ChatAIMessageWidget extends StatelessWidget { final String? refSourceJsonString; final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; final void Function()? onRegenerate; + final void Function() onStopStream; final void Function(PredefinedFormat)? onChangeFormat; final bool isStreaming; final bool isLastMessage; @@ -126,26 +128,39 @@ class ChatAIMessageWidget extends StatelessWidget { ); }, onError: (error) { + onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.chat_aiServerUnavailable.tr(), ); }, onAIResponseLimit: () { + onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(), ); }, onAIImageResponseLimit: () { + onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(), ); }, onAIMaxRequired: (message) { + onStopStream(); return ChatErrorMessageWidget( errorMessage: message, ); }, + onInitializingLocalAI: () { + onStopStream(); + + return ChatErrorMessageWidget( + errorMessage: LocaleKeys + .settings_aiPage_keys_localAIInitializing + .tr(), + ); + }, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart index 5ed82c601a..f6d5ef949d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart @@ -13,7 +13,7 @@ part 'local_ai_setting_panel_bloc.freezed.dart'; class LocalAISettingPanelBloc extends Bloc { LocalAISettingPanelBloc() - : listener = LocalLLMListener(), + : listener = LocalAIStateListener(), super(const LocalAISettingPanelState()) { on(_handleEvent); @@ -35,7 +35,7 @@ class LocalAISettingPanelBloc ); } - final LocalLLMListener listener; + final LocalAIStateListener listener; /// Handles incoming events and dispatches them to the appropriate handler. Future _handleEvent( diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart index 40cdb564b2..99c90faeb5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -11,8 +11,8 @@ import 'package:appflowy_result/appflowy_result.dart'; typedef PluginStateCallback = void Function(LocalAIPB state); typedef PluginResourceCallback = void Function(LackOfAIResourcePB data); -class LocalLLMListener { - LocalLLMListener() { +class LocalAIStateListener { + LocalAIStateListener() { _parser = ChatNotificationParser(id: "appflowy_ai_plugin", callback: _callback); _subscription = RustStreamReceiver.listen( diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart index f9727dfc89..4c3130ea00 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -11,7 +11,7 @@ part 'plugin_state_bloc.freezed.dart'; class PluginStateBloc extends Bloc { PluginStateBloc() - : listener = LocalLLMListener(), + : listener = LocalAIStateListener(), super( const PluginStateState( action: PluginStateAction.unknown(), @@ -33,7 +33,7 @@ class PluginStateBloc extends Bloc { on(_handleEvent); } - final LocalLLMListener listener; + final LocalAIStateListener listener; @override Future close() async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 46935a2450..19806c52ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; @@ -17,64 +16,54 @@ class LocalAISetting extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.aiSettings == null) { - return const SizedBox.shrink(); - } + return BlocProvider( + create: (context) => + LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ExpandableNotifier( + child: BlocListener( + listener: (context, state) { + final controller = + ExpandableController.of(context, required: true)!; - return BlocProvider( - create: (context) => - LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ExpandableNotifier( - child: BlocListener( - listener: (context, state) { - final controller = - ExpandableController.of(context, required: true)!; - - state.pageIndicator.when( - error: (_) => controller.expanded = true, - isEnabled: (enabled) => controller.expanded = enabled, - loading: () => controller.expanded = true, - ); - }, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, + state.pageIndicator.when( + error: (_) => controller.expanded = true, + isEnabled: (enabled) => controller.expanded = enabled, + loading: () => controller.expanded = true, + ); + }, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const LocalAISettingHeader(), + collapsed: const SizedBox.shrink(), + expanded: Column( + children: [ + const VSpace(6), + DecoratedBox( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: const Padding( + padding: + EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: LocalAISettingPanel(), + ), ), - header: const LocalAISettingHeader(), - collapsed: const SizedBox.shrink(), - expanded: Column( - children: [ - const VSpace(6), - DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, - borderRadius: - const BorderRadius.all(Radius.circular(4)), - ), - child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: LocalAISettingPanel(), - ), - ), - ], - ), - ), + ], ), ), ), - ); - }, + ), + ), ); } } @@ -87,12 +76,8 @@ class LocalAISettingHeader extends StatelessWidget { return BlocBuilder( builder: (context, state) { return state.pageIndicator.when( - error: (error) { - return const SizedBox.shrink(); - }, - loading: () { - return const SizedBox.shrink(); - }, + error: (error) => SizedBox.shrink(), + loading: () => const SizedBox.shrink(), isEnabled: (isEnabled) { return Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 14ecfb3708..65ed04c0df 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -55,13 +55,7 @@ class SettingsAIView extends StatelessWidget { ]; children.add(const _AISearchToggle(value: false)); - children.add( - _LocalAIOnBoarding( - userProfile: userProfile, - currentWorkspaceMemberRole: state.currentWorkspaceMemberRole!, - workspaceId: workspaceId, - ), - ); + children.add(const LocalAISetting()); return SettingsBody( title: LocaleKeys.settings_aiPage_title.tr(), diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 31caa5ce69..6a4ea2102d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e#8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f1b5167e9569e8a61ef50a1afb140306a5287e57#f1b5167e9569e8a61ef50a1afb140306a5287e57" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e#8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f1b5167e9569e8a61ef50a1afb140306a5287e57#f1b5167e9569e8a61ef50a1afb140306a5287e57" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 662703340b..a9142cd9d2 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8fea7ed2375eb54c8dfb8af6db6e32a61854fb2e" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f1b5167e9569e8a61ef50a1afb140306a5287e57" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f1b5167e9569e8a61ef50a1afb140306a5287e57" } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 17ffb29a0d..93ed5ba858 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -263,6 +263,10 @@ impl Chat { let _ = answer_sink .send(format!("AI_MAX_REQUIRED:{}", err.msg)) .await; + } else if err.is_local_ai_not_ready() { + let _ = answer_sink + .send(format!("LOCAL_AI_NOT_READY:{}", err.msg)) + .await; } else { let _ = answer_sink.send(format!("error:{}", err)).await; } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 730888d7c5..f6e0750970 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -72,6 +72,10 @@ impl LocalAIController { user_service: Arc, cloud_service: Arc, ) -> Self { + debug!( + "[AI Plugin] init local ai controller, thread: {:?}", + std::thread::current().id() + ); let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), @@ -176,7 +180,7 @@ impl LocalAIController { if !self.is_enabled() { return false; } - self.ai_plugin.get_plugin_running_state().is_ready() + self.ai_plugin.get_plugin_running_state().is_running() } /// Indicate whether the local AI is enabled. diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 6fb41c84f2..686637cb3e 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -158,18 +158,22 @@ impl ChatCloudService for AICloudServiceMiddleware { question_id: i64, format: ResponseFormat, ) -> Result { - if self.local_ai.is_running() { - let row = self.get_message_record(question_id)?; - match self - .local_ai - .stream_question(chat_id, &row.content, json!({})) - .await - { - Ok(stream) => Ok(QuestionStream::new(stream).boxed()), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, + if self.local_ai.is_enabled() { + if self.local_ai.is_running() { + let row = self.get_message_record(question_id)?; + match self + .local_ai + .stream_question(chat_id, &row.content, json!({})) + .await + { + Ok(stream) => Ok(QuestionStream::new(stream).boxed()), + Err(err) => { + self.handle_plugin_error(err); + Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) + }, + } + } else { + Err(FlowyError::local_ai_not_ready()) } } else { self diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 85822f5e02..dad5f84e38 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -371,6 +371,9 @@ pub enum ErrorCode { #[error("Request timeout")] RequestTimeout = 127, + + #[error("Local AI is not ready")] + LocalAINotReady = 128, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index e7b6fdd439..0a6721a31a 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -95,6 +95,10 @@ impl FlowyError { self.code == ErrorCode::AIImageResponseLimitExceeded } + pub fn is_local_ai_not_ready(&self) -> bool { + self.code == ErrorCode::LocalAINotReady + } + pub fn is_ai_max_required(&self) -> bool { self.code == ErrorCode::AIMaxRequired } @@ -151,6 +155,7 @@ impl FlowyError { static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded); static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); + static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); } impl std::convert::From for FlowyError { From 555254e8febda918d74ababb6874db4beb0c5f65 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 12 Mar 2025 21:46:06 +0800 Subject: [PATCH 123/384] chore: add ai writer keyboard shortcuts (#7516) --- .../lib/plugins/ai_chat/chat_page.dart | 17 ++++- .../ai/ai_writer_block_component.dart | 25 +++++-- .../ai/operations/ai_writer_cubit.dart | 74 +++++++++++++++---- .../widgets/ai_writer_gesture_detector.dart | 14 ++-- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 5221ab3766..74d1364436 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -12,6 +12,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart' @@ -92,9 +93,19 @@ class AIChatPage extends StatelessWidget { } } }, - child: _ChatContentPage( - view: view, - userProfile: userProfile, + child: CallbackShortcuts( + bindings: { + SingleActivator(LogicalKeyboardKey.escape): () { + context.read().add(ChatEvent.stopStream()); + }, + SingleActivator(control: true, LogicalKeyboardKey.keyC): () { + context.read().add(ChatEvent.stopStream()); + }, + }, + child: _ChatContentPage( + view: view, + userProfile: userProfile, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index ed77fadae4..aee2e2cb50 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -100,7 +100,6 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget { class _AIWriterBlockComponentState extends State { final key = GlobalKey(); final textController = TextEditingController(); - final textFieldFocusNode = FocusNode(); final overlayController = OverlayPortalController(); final layerLink = LayerLink(); @@ -118,7 +117,6 @@ class _AIWriterBlockComponentState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { overlayController.show(); - textFieldFocusNode.requestFocus(); if (!widget.node.isAiWriterInitialized) { aiWriterCubit.init(); } @@ -128,7 +126,6 @@ class _AIWriterBlockComponentState extends State { @override void dispose() { textController.dispose(); - textFieldFocusNode.dispose(); aiWriterCubit.close(); super.dispose(); } @@ -154,12 +151,24 @@ class _AIWriterBlockComponentState extends State { builder: (context, constraints) { return BlocListener( listener: (context, state) { - if (state is SingleShotAiWriterState) { + if (state is FailedContinueWritingAiWriterState) { showConfirmDialog( context: context, - title: state.title, - description: state.description, - onConfirm: state.onDismiss, + title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: LocaleKeys + .ai_continueWritingEmptyDocumentDescription + .tr(), + onConfirm: state.onConfirm, + ); + } else if (state is DiscardResponseAiWriterState) { + showConfirmDialog( + context: context, + title: LocaleKeys.button_discard.tr(), + description: LocaleKeys.document_plugins_discardResponse.tr(), + confirmLabel: LocaleKeys.button_discard.tr(), + style: ConfirmPopupStyle.cancelAndOk, + onConfirm: state.onDiscard, + onCancel: () {}, ); } }, @@ -174,7 +183,7 @@ class _AIWriterBlockComponentState extends State { behavior: state is GeneratingAiWriterState ? HitTestBehavior.opaque : HitTestBehavior.translucent, - onPointerEvent: () => onTapOutside(), + onPointerEvent: onTapOutside, ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 56cdacdd6a..d31dc45dcd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -10,6 +10,7 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import '../../base/markdown_text_robot.dart'; import 'ai_writer_block_operations.dart'; @@ -32,6 +33,7 @@ class AiWriterCubit extends Cubit { isFirstRun: true, ), ) { + HardwareKeyboard.instance.addHandler(_cancelShortcutHandler); editorState.service.keyboardService?.disableShortcuts(); } @@ -49,6 +51,7 @@ class AiWriterCubit extends Cubit { @override Future close() async { selectedSourcesNotifier.dispose(); + HardwareKeyboard.instance.removeHandler(_cancelShortcutHandler); editorState.service.keyboardService?.enableShortcuts(); await super.close(); } @@ -147,6 +150,9 @@ class AiWriterCubit extends Cubit { if (state is! GeneratingAiWriterState) { return; } + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); final generatingState = state as GeneratingAiWriterState; await AIEventStopCompleteText( CompleteTextTaskPB( @@ -311,12 +317,9 @@ class AiWriterCubit extends Cubit { view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) { final readyState = state as ReadyAiWriterState; emit( - SingleShotAiWriterState( + FailedContinueWritingAiWriterState( command, - title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: - LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(), - onDismiss: () { + onConfirm: () { if (isImmediateRun) { removeAiWriterNode(editorState, node); } @@ -486,6 +489,46 @@ class AiWriterCubit extends Cubit { ); } } + + bool _cancelShortcutHandler(KeyEvent event) { + if (event is! KeyUpEvent) { + return false; + } + + switch (event.logicalKey) { + case LogicalKeyboardKey.escape: + if (state case GeneratingAiWriterState _) { + stopStream(); + } else if (hasUnusedResponse()) { + final saveState = state; + emit( + FailedContinueWritingAiWriterState( + state.command, + onConfirm: () { + stopStream(); + exit(); + }, + ), + ); + emit(saveState); + } else { + stopStream(); + exit(); + } + return true; + case LogicalKeyboardKey.keyC + when HardwareKeyboard.instance.logicalKeysPressed + .contains(LogicalKeyboardKey.controlLeft): + if (state case GeneratingAiWriterState _) { + stopStream(); + } + return true; + default: + break; + } + + return false; + } } sealed class AiWriterState { @@ -527,15 +570,20 @@ class ErrorAiWriterState extends AiWriterState { final AIError error; } -class SingleShotAiWriterState extends AiWriterState { - const SingleShotAiWriterState( +class FailedContinueWritingAiWriterState extends AiWriterState { + const FailedContinueWritingAiWriterState( super.command, { - required this.title, - required this.description, - required this.onDismiss, + required this.onConfirm, }); - final String title; - final String description; - final void Function() onDismiss; + final void Function() onConfirm; +} + +class DiscardResponseAiWriterState extends AiWriterState { + const DiscardResponseAiWriterState( + super.command, { + required this.onDiscard, + }); + + final void Function() onDiscard; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart index 6b2ffd4692..84f785335b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart @@ -21,18 +21,14 @@ class AiWriterGestureDetector extends StatelessWidget { TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), - (instance) { - instance - ..onTap = onPointerEvent - ..onTapDown = (_) => onPointerEvent(); - }, + (instance) => instance..onTapDown = (_) => onPointerEvent(), ), ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< - ImmediateMultiDragGestureRecognizer>( - () => ImmediateMultiDragGestureRecognizer(), (instance) { - instance.onStart = (offset) => null; - }), + ImmediateMultiDragGestureRecognizer>( + () => ImmediateMultiDragGestureRecognizer(), + (instance) => instance.onStart = (offset) => null, + ), }, child: child, ); From 1f76412790ce5219b27fb4d4c79b5dc441f6f03d Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 13 Mar 2025 09:26:56 +0800 Subject: [PATCH 124/384] fix: nested list issue in quote/callout block (#7513) --- .../presentation/editor_configuration.dart | 24 ++++++++----------- .../document/presentation/editor_page.dart | 4 +++- .../actions/block_action_option_cubit.dart | 5 ++-- .../actions/drag_to_reorder/util.dart | 5 ---- .../callout/callout_block_component.dart | 18 +++++++++++++- .../simple_column_block_component.dart | 4 ---- .../parsers/callout_node_parser.dart | 1 - .../quote/quote_block_component.dart | 9 +++++-- .../quote/quote_block_shortcuts.dart | 7 ++++-- .../simple_table_enter_command.dart | 5 +++- .../lib/startup/tasks/rust_sdk.dart | 2 -- .../document/turn_into/turn_into_test.dart | 8 ++----- 12 files changed, 50 insertions(+), 42 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index c22cbb648c..3889a7d1d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -37,6 +37,7 @@ final Set supportSlashMenuNodeTypes = { NumberedListBlockKeys.type, QuoteBlockKeys.type, ToggleListBlockKeys.type, + CalloutBlockKeys.type, // Simple table SimpleTableBlockKeys.type, @@ -130,7 +131,8 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) { // in the quote block, we reduce the indent padding for the first level block. // So we have to add more padding for the second level to avoid the drag menu overlay the quote icon. - if (node.isInQuote && node.level == 2 && UniversalPlatform.isDesktop) { + if (node.parent?.type == QuoteBlockKeys.type && + UniversalPlatform.isDesktop) { padding += 22; } @@ -615,6 +617,13 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( if (UniversalPlatform.isMobile) { return configuration.indentPadding(node, textDirection); } + + if (node.isInTable) { + return textDirection == TextDirection.ltr + ? EdgeInsets.only(left: 24) + : EdgeInsets.only(right: 24); + } + return EdgeInsets.zero; }, ), @@ -1096,16 +1105,3 @@ TextAlign _buildTextAlignInTableCell( return node.tableAlign.textAlign; } - -extension on Node { - bool get isInQuote { - Node? parent = this.parent; - while (parent != null) { - if (parent.type == QuoteBlockKeys.type) { - return true; - } - parent = parent.parent; - } - return false; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 9c5302bcd8..97cceb1aa8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -19,7 +19,7 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; @@ -158,10 +158,12 @@ class _AppFlowyEditorPageState extends State indentableBlockTypes.addAll([ ToggleListBlockKeys.type, CalloutBlockKeys.type, + QuoteBlockKeys.type, ]); convertibleBlockTypes.addAll([ ToggleListBlockKeys.type, CalloutBlockKeys.type, + QuoteBlockKeys.type, ]); editorLaunchUrl = (url) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index 733da3fd71..f3074781b4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -322,9 +322,8 @@ class BlockActionOptionCubit extends Cubit { }, ); - // heading block and callout block should not have children - if ([HeadingBlockKeys.type, CalloutBlockKeys.type, QuoteBlockKeys.type] - .contains(toType)) { + // heading block should not have children + if ([HeadingBlockKeys.type].contains(toType)) { afterNode = afterNode.copyWith(children: []); afterNode = await _handleSubPageNode(afterNode, node); insertedNode.add(afterNode); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index c816934976..ca99491b94 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -188,7 +188,6 @@ Future dragToMoveNode( Node dragTargetNode, Offset dragOffset, ) { - debugPrint('getDragAreaPosition - dragTargetNode: ${dragTargetNode.type}'); final selectable = dragTargetNode.selectable; final renderBox = selectable?.context.findRenderObject() as RenderBox?; if (selectable == null || renderBox == null) { @@ -247,10 +246,6 @@ Future dragToMoveNode( verticalPosition = VerticalPosition.bottom; } - debugPrint( - 'verticalPosition: $verticalPosition, horizontalPosition: $horizontalPosition', - ); - return (verticalPosition, horizontalPosition, globalBlockRect); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 1e8d981c15..522b694edb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -185,6 +185,22 @@ class _CalloutBlockComponentWidgetState return result; } + @override + Widget build(BuildContext context) { + Widget child = node.children.isEmpty + ? buildComponent(context) + : buildComponentWithChildren(context); + + if (UniversalPlatform.isDesktop) { + child = Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + child: child, + ); + } + + return child; + } + @override Widget buildComponentWithChildren(BuildContext context) { Widget child = Stack( @@ -287,7 +303,7 @@ class _CalloutBlockComponentWidgetState child: child, ); } else { - child = Padding( + child = Container( key: blockComponentKey, padding: EdgeInsets.zero, child: child, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart index a7259d54b1..979dd92ce0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -119,9 +118,6 @@ class SimpleColumnBlockComponentState extends State Widget child = IntrinsicHeight( child: editorState.renderer.build(context, e), ); - if (e.type == CustomImageBlockKeys.type) { - child = IntrinsicWidth(child: child); - } if (SimpleColumnsBlockConstants.enableDebugBorder) { child = DecoratedBox( decoration: BoxDecoration( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index b16a44cf6f..d9cf060e3b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -10,7 +10,6 @@ class CalloutNodeParser extends NodeParser { @override String transform(Node node, DocumentMarkdownEncoder? encoder) { - assert(node.children.isEmpty); final delta = node.delta ?? Delta() ..insert(''); final String markdown = DeltaMarkdownEncoder() diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart index 211a99c1bf..76f42c54b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart @@ -267,8 +267,13 @@ class _QuoteBlockComponentWidgetState extends State WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final renderObject = layoutBuilderKey.currentContext?.findRenderObject(); if (renderObject != null && renderObject is RenderBox) { - quoteBlockHeightNotifier.value = - renderObject.size.height - padding.top * 2; + if (UniversalPlatform.isMobile) { + quoteBlockHeightNotifier.value = + renderObject.size.height - padding.top; + } else { + quoteBlockHeightNotifier.value = + renderObject.size.height - padding.top * 2; + } } else { quoteBlockHeightNotifier.value = 0; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart index 835d7bb47a..914aa7163c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart @@ -31,7 +31,9 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { if (HardwareKeyboard.instance.isShiftPressed) { await editorState.insertNewLine(); - } else if (node.children.isEmpty) { + return true; + } else if (node.children.isEmpty && + selection.endIndex == node.delta?.length) { // insert a new paragraph within the callout block final path = node.path.child(0); final transaction = editorState.transaction; @@ -45,7 +47,8 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { ), ); await editorState.apply(transaction); + return true; } - return true; + return false; }; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart index 74bdc3fc62..bfc31e8abc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -40,7 +41,9 @@ KeyEventResult _enterInTableCellHandler(EditorState editorState) { return KeyEventResult.handled; } } - return convertToParagraphCommand.execute(editorState); + if (node.type != CalloutBlockKeys.type) { + return convertToParagraphCommand.execute(editorState); + } } return KeyEventResult.ignored; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index a0f5b0bafe..55f2f53512 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -5,7 +5,6 @@ import 'package:appflowy/env/backend_env.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -29,7 +28,6 @@ class InitRustSDKTask extends LaunchTask { final dir = customApplicationPath ?? applicationPath; final deviceId = await getDeviceId(); - debugPrint('application path: ${applicationPath.path}'); // Pass the environment variables to the Rust SDK final env = _makeAppFlowyConfiguration( root.path, diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart index d4b0bfa45c..adf2857edb 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart @@ -164,8 +164,6 @@ void main() { for (final type in [ HeadingBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, ]) { test('from nested bulleted list to $type', () async { const text = 'bulleted list'; @@ -230,8 +228,6 @@ void main() { for (final type in [ HeadingBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, ]) { test('from nested numbered list to $type', () async { const text = 'numbered list'; @@ -296,8 +292,6 @@ void main() { for (final type in [ HeadingBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, ]) { // numbered list, bulleted list, todo list // before @@ -392,6 +386,8 @@ void main() { BulletedListBlockKeys.type, NumberedListBlockKeys.type, TodoListBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, ]) { // numbered list, bulleted list, todo list // before From 392964ffd2b96e0bed753a2c7ac81ca334e40dcd Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:50:46 +0800 Subject: [PATCH 125/384] chore: disable add messages to page when chat is empty (#7518) --- .../application/chat_select_message_bloc.dart | 25 ++++++++++++++++--- .../lib/plugins/ai_chat/chat.dart | 1 + .../lib/plugins/ai_chat/chat_page.dart | 4 +++ .../chat_message_selector_banner.dart | 4 +-- .../widgets/common_view_action.dart | 11 +++++--- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart index 90a2db168f..9977d1df72 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart @@ -19,9 +19,17 @@ class ChatSelectMessageBloc on( (event, emit) { event.when( + enableStartSelectingMessages: () { + emit(state.copyWith(enabled: true)); + }, toggleSelectingMessages: () { if (state.isSelectingMessages) { - emit(ChatSelectMessageState.initial()); + emit( + state.copyWith( + isSelectingMessages: false, + selectedMessages: [], + ), + ); } else { emit(state.copyWith(isSelectingMessages: true)); } @@ -50,8 +58,13 @@ class ChatSelectMessageBloc unselectAllMessages: () { emit(state.copyWith(selectedMessages: const [])); }, - saveAsPage: () { - emit(ChatSelectMessageState.initial()); + reset: () { + emit( + state.copyWith( + isSelectingMessages: false, + selectedMessages: [], + ), + ); }, ); }, @@ -70,6 +83,8 @@ class ChatSelectMessageBloc @freezed class ChatSelectMessageEvent with _$ChatSelectMessageEvent { + const factory ChatSelectMessageEvent.enableStartSelectingMessages() = + _EnableStartSelectingMessages; const factory ChatSelectMessageEvent.toggleSelectingMessages() = _ToggleSelectingMessages; const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) = @@ -79,7 +94,7 @@ class ChatSelectMessageEvent with _$ChatSelectMessageEvent { ) = _SelectAllMessages; const factory ChatSelectMessageEvent.unselectAllMessages() = _UnselectAllMessages; - const factory ChatSelectMessageEvent.saveAsPage() = _SaveAsPage; + const factory ChatSelectMessageEvent.reset() = _Reset; } @freezed @@ -87,9 +102,11 @@ class ChatSelectMessageState with _$ChatSelectMessageState { const factory ChatSelectMessageState({ required bool isSelectingMessages, required List selectedMessages, + required bool enabled, }) = _ChatSelectMessageState; factory ChatSelectMessageState.initial() => const ChatSelectMessageState( + enabled: false, isSelectingMessages: false, selectedMessages: [], ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index 1b3880e01d..b840472854 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -178,6 +178,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder customActions: [ CustomViewAction( view: notifier.view, + disabled: !state.enabled, leftIcon: FlowySvgs.ai_add_to_page_s, label: LocaleKeys.moreAction_saveAsNewPage.tr(), onTap: () { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 74d1364436..2d0c05b2c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -319,6 +319,10 @@ class _ChatContentPage extends StatelessWidget { ); } + context + .read() + .add(ChatSelectMessageEvent.enableStartSelectingMessages()); + return BlocSelector( selector: (state) => state.isSelectingMessages, builder: (context, isSelectingMessages) { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart index 4b25297d63..790a3fac3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart @@ -250,7 +250,7 @@ class _SaveToPageButtonState extends State { showSaveMessageSuccessToast(context, view); } - bloc.add(const ChatSelectMessageEvent.saveAsPage()); + bloc.add(const ChatSelectMessageEvent.reset()); return view; } @@ -275,7 +275,7 @@ class _SaveToPageButtonState extends State { showSaveMessageSuccessToast(context, newView); openPageFromMessage(context, newView); } - bloc.add(const ChatSelectMessageEvent.saveAsPage()); + bloc.add(const ChatSelectMessageEvent.reset()); } Future forceReload(String documentId) async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart index 10bc3dde34..6fad559ba4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart @@ -107,6 +107,7 @@ class CustomViewAction extends StatelessWidget { required this.view, required this.leftIcon, required this.label, + this.disabled = false, this.onTap, this.mutex, }); @@ -114,6 +115,7 @@ class CustomViewAction extends StatelessWidget { final ViewPB view; final FlowySvgData leftIcon; final String label; + final bool disabled; final VoidCallback? onTap; final PopoverMutex? mutex; @@ -122,17 +124,20 @@ class CustomViewAction extends StatelessWidget { return Container( height: 34, padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( + child: FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 6), + disable: disabled, onTap: onTap, - leftIconBuilder: (onHover) => FlowySvg( + leftIcon: FlowySvg( leftIcon, size: const Size.square(16.0), + color: disabled ? Theme.of(context).disabledColor : null, ), iconPadding: 10.0, - textBuilder: (onHover) => FlowyText( + text: FlowyText( label, figmaLineHeight: 18.0, + color: disabled ? Theme.of(context).disabledColor : null, ), ), ); From caaf5f7986f058acf14f4f73072f488d6596bedd Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 13 Mar 2025 10:27:11 +0800 Subject: [PATCH 126/384] chore: remove deprecated pem files (#7521) --- frontend/appflowy_flutter/dsa_priv.pem | 26 ------------------- frontend/appflowy_flutter/dsa_pub.pem | 36 -------------------------- 2 files changed, 62 deletions(-) delete mode 100644 frontend/appflowy_flutter/dsa_priv.pem delete mode 100644 frontend/appflowy_flutter/dsa_pub.pem diff --git a/frontend/appflowy_flutter/dsa_priv.pem b/frontend/appflowy_flutter/dsa_priv.pem deleted file mode 100644 index 66843054b7..0000000000 --- a/frontend/appflowy_flutter/dsa_priv.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEXAIBADCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0Y -ruaTrrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7t -J8mG4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy -7xyw+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL -7iTVKiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqH -Opf5b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdp -qm4ZQRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFy -JiJWYWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ -5EhGG4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAh -Lswu6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPh -AsVA6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxC -xMTpq1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIA -Pxbd0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+V -uix/4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/ -8WrbK13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqT -QJg7hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19 -tKcOs6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQd -bsCzAxp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzf -J4v4uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6T -jcfVWthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NCl -WgZnixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3w -m7NB+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcT -ilaNC9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkEHgIcfy0+ZHp+4MBcWSDv -uzWeM8QmNvbP+owM+H4F7A== ------END PRIVATE KEY----- diff --git a/frontend/appflowy_flutter/dsa_pub.pem b/frontend/appflowy_flutter/dsa_pub.pem deleted file mode 100644 index 6a9d213b8a..0000000000 --- a/frontend/appflowy_flutter/dsa_pub.pem +++ /dev/null @@ -1,36 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT -rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG -4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw -+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV -KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5 -b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z -QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW -YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG -G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu -6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA -6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp -q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd -0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/ -4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb -K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7 -hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO -s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz -Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4 -uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV -Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn -ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB -+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN -C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r -vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx -k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y -GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/ -eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG -hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM -EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8 -iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI -7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb -w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf -1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P -Y29SB4jvwqls268rP0cWqy4WXwlVwuc= ------END PUBLIC KEY----- From ee69283a23c4cf1bf5735264e87d051f342e659f Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 10:53:20 +0800 Subject: [PATCH 127/384] chore: support response format --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 2 +- frontend/rust-lib/flowy-ai/src/chat.rs | 2 +- .../rust-lib/flowy-ai/src/local_ai/controller.rs | 2 +- .../flowy-ai/src/middleware/chat_service_mw.rs | 14 +++++++++----- .../src/deps_resolve/cloud_service_impl.rs | 4 ++-- .../flowy-server/src/af_cloud/impls/chat.rs | 2 +- frontend/rust-lib/flowy-server/src/default_impl.rs | 2 +- 9 files changed, 20 insertions(+), 16 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6a4ea2102d..672e355983 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f1b5167e9569e8a61ef50a1afb140306a5287e57#f1b5167e9569e8a61ef50a1afb140306a5287e57" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cf0b5e77d3bbcecbcd9cbed86476658b477399e6#cf0b5e77d3bbcecbcd9cbed86476658b477399e6" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f1b5167e9569e8a61ef50a1afb140306a5287e57#f1b5167e9569e8a61ef50a1afb140306a5287e57" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cf0b5e77d3bbcecbcd9cbed86476658b477399e6#cf0b5e77d3bbcecbcd9cbed86476658b477399e6" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index a9142cd9d2..0a732ec2a2 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f1b5167e9569e8a61ef50a1afb140306a5287e57" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f1b5167e9569e8a61ef50a1afb140306a5287e57" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cf0b5e77d3bbcecbcd9cbed86476658b477399e6" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cf0b5e77d3bbcecbcd9cbed86476658b477399e6" } diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 98198c8f9f..f8fdeb212d 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -92,7 +92,7 @@ pub trait ChatCloudService: Send + Sync + 'static { params: CompleteTextParams, ) -> Result; - async fn index_file( + async fn embed_file( &self, workspace_id: &str, file_path: &Path, diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 93ed5ba858..3156053ae2 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -587,7 +587,7 @@ impl Chat { ); self .chat_service - .index_file( + .embed_file( &self.user_service.workspace_id()?, &file_path, &self.chat_id, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index f6e0750970..8fa27ccc26 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -392,7 +392,7 @@ impl LocalAIController { .await; let result = self - .index_file(chat_id, file_path, content, Some(index_metadata.clone())) + .embed_file(chat_id, file_path, content, Some(index_metadata.clone())) .await; match result { Ok(_) => { diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 686637cb3e..7ebe5889a7 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -163,7 +163,7 @@ impl ChatCloudService for AICloudServiceMiddleware { let row = self.get_message_record(question_id)?; match self .local_ai - .stream_question(chat_id, &row.content, json!({})) + .stream_question(chat_id, &row.content, Some(json!(format)), json!({})) .await { Ok(stream) => Ok(QuestionStream::new(stream).boxed()), @@ -277,7 +277,11 @@ impl ChatCloudService for AICloudServiceMiddleware { if self.local_ai.is_running() { match self .local_ai - .complete_text(¶ms.text, params.completion_type.unwrap() as u8) + .complete_text( + ¶ms.text, + params.completion_type.unwrap() as u8, + Some(json!(params.format)), + ) .await { Ok(stream) => Ok( @@ -298,7 +302,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn index_file( + async fn embed_file( &self, workspace_id: &str, file_path: &Path, @@ -308,14 +312,14 @@ impl ChatCloudService for AICloudServiceMiddleware { if self.local_ai.is_running() { self .local_ai - .index_file(chat_id, Some(file_path.to_path_buf()), None, metadata) + .embed_file(chat_id, Some(file_path.to_path_buf()), None, metadata) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; Ok(()) } else { self .cloud_service - .index_file(workspace_id, file_path, chat_id, metadata) + .embed_file(workspace_id, file_path, chat_id, metadata) .await } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 22342615e3..80c22642a4 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -781,7 +781,7 @@ impl ChatCloudService for ServerProvider { .await } - async fn index_file( + async fn embed_file( &self, workspace_id: &str, file_path: &Path, @@ -791,7 +791,7 @@ impl ChatCloudService for ServerProvider { self .get_server()? .chat_service() - .index_file(workspace_id, file_path, chat_id, metadata) + .embed_file(workspace_id, file_path, chat_id, metadata) .await } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 11fc5cc27c..c7077dc061 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -200,7 +200,7 @@ where Ok(stream.boxed()) } - async fn index_file( + async fn embed_file( &self, _workspace_id: &str, _file_path: &Path, diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 194aa89ef3..d1a16159c4 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -101,7 +101,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) } - async fn index_file( + async fn embed_file( &self, _workspace_id: &str, _file_path: &Path, From f0b8b004610ba374a35663abe09255ef5f9dcc15 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 13:45:01 +0800 Subject: [PATCH 128/384] chore: force restart plugin --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- .../rust-lib/flowy-ai/src/local_ai/controller.rs | 15 ++++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 672e355983..c7d563e256 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cf0b5e77d3bbcecbcd9cbed86476658b477399e6#cf0b5e77d3bbcecbcd9cbed86476658b477399e6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0#cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cf0b5e77d3bbcecbcd9cbed86476658b477399e6#cf0b5e77d3bbcecbcd9cbed86476658b477399e6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0#cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 0a732ec2a2..ccbfbca269 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cf0b5e77d3bbcecbcd9cbed86476658b477399e6" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cf0b5e77d3bbcecbcd9cbed86476658b477399e6" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 8fa27ccc26..11a0e9a480 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -141,7 +141,7 @@ impl LocalAIController { resource: &Arc, ai_plugin: &Arc, ) { - if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None).await { + if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None, false).await { error!("[AI Plugin] failed to setup plugin: {:?}", err); } } @@ -278,7 +278,7 @@ impl LocalAIController { #[instrument(level = "debug", skip_all)] pub async fn restart_plugin(&self) { - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None, true).await { error!("[AI Plugin] failed to setup plugin: {:?}", err); } } @@ -430,7 +430,8 @@ impl LocalAIController { ); if enabled { let (tx, rx) = tokio::sync::oneshot::channel(); - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx)).await { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx), false).await + { error!("[AI Plugin] failed to initialize local ai: {:?}", err); } let _ = rx.await; @@ -460,10 +461,14 @@ async fn initialize_ai_plugin( plugin: &Arc, llm_resource: &Arc, ret: Option>, + force: bool, ) -> FlowyResult<()> { let plugin = plugin.clone(); - if plugin.get_plugin_running_state().is_loading() { - return Ok(()); + + if !force { + if plugin.get_plugin_running_state().is_loading() { + return Ok(()); + } } let lack_of_resource = llm_resource.get_lack_of_resource().await; From 69dd2ab20fe7558285aaba11cd97d60956edcb3c Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 13 Mar 2025 13:51:03 +0800 Subject: [PATCH 129/384] feat: revamp toolbar UI (#7506) * feat: revamp toolbar UI * fix: integration test issues * feat: add suggestions for toolbar * feat: support dark mode * chore: update editor dependency * feat: add testing for suggestions * chore: update editor dependency --- .../document/document_alignment_test.dart | 15 +- .../document/document_toolbar_test.dart | 159 +++++- ...cument_with_inline_math_equation_test.dart | 42 +- ...cument_with_toggle_heading_block_test.dart | 11 +- .../shared/database_test_op.dart | 21 +- .../board/presentation/board_page.dart | 2 +- .../setting/database_setting_action.dart | 4 +- .../document/presentation/editor_page.dart | 49 +- .../ai/ai_writer_toolbar_item.dart | 76 +-- .../base/toolbar_extension.dart | 24 + .../font/customize_font_toolbar_item.dart | 145 ++---- .../inline_math_equation_toolbar_item.dart | 2 +- .../custom_format_toolbar_items.dart | 89 ++++ .../custom_hightlight_color_toolbar_item.dart | 75 +++ .../custom_link_toolbar_item.dart | 45 ++ .../custom_text_align_toolbar_item.dart | 198 ++++++++ .../custom_text_color_toolbar_item.dart | 76 +++ .../more_option_toolbar_item.dart | 324 ++++++++++++ .../text_heading_toolbar_item.dart | 313 ++++++++++++ .../text_suggestions_toolbar_item.dart | 479 ++++++++++++++++++ .../document/presentation/editor_style.dart | 22 +- .../lib/plugins/shared/share/export_tab.dart | 2 +- .../appearance/desktop_appearance.dart | 1 + .../appearance/mobile_appearance.dart | 1 + .../shared/setting_value_dropdown.dart | 16 +- .../lib/colorscheme/colorscheme.dart | 2 + .../lib/colorscheme/dandelion.dart | 2 + .../lib/colorscheme/default_colorscheme.dart | 2 + .../flowy_infra/lib/colorscheme/lavender.dart | 2 + .../flowy_infra/lib/colorscheme/lemonade.dart | 111 ++-- .../flowy_infra/lib/theme_extension.dart | 6 + .../src/flowy_overlay/appflowy_popover.dart | 2 +- .../lib/style_widget/toolbar_button.dart | 43 ++ .../lib/widget/flowy_tooltip.dart | 13 +- .../packages/flowy_svg/bin/flowy_svg.dart | 8 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- .../test/widget_test/test_material_app.dart | 1 + .../{24x => 16x}/calendar_layout.svg | 0 .../flowy_icons/{24x => 16x}/close_filled.svg | 0 .../{24x => 16x}/database_layout.svg | 0 .../20x/toolbar_ai_improve_writing.svg | 4 + .../flowy_icons/20x/toolbar_ai_writer.svg | 4 + .../flowy_icons/20x/toolbar_alignment.svg | 3 + .../flowy_icons/20x/toolbar_arrow_down.svg | 3 + .../flowy_icons/20x/toolbar_arrow_right.svg | 3 + .../flowy_icons/20x/toolbar_bold.svg | 3 + .../flowy_icons/20x/toolbar_check.svg | 3 + .../flowy_icons/20x/toolbar_inline_code.svg | 3 + .../flowy_icons/20x/toolbar_inline_italic.svg | 3 + .../flowy_icons/20x/toolbar_link.svg | 3 + .../flowy_icons/20x/toolbar_more.svg | 5 + .../20x/toolbar_text_align_center.svg | 3 + .../20x/toolbar_text_align_left.svg | 3 + .../20x/toolbar_text_align_right.svg | 3 + .../flowy_icons/20x/toolbar_text_color.svg | 3 + .../flowy_icons/20x/toolbar_text_format.svg | 3 + .../20x/toolbar_text_highlight.svg | 3 + .../flowy_icons/20x/toolbar_underline.svg | 3 + .../flowy_icons/20x/type_bulleted_list.svg | 6 + .../flowy_icons/20x/type_callout.svg | 3 + .../resources/flowy_icons/20x/type_font.svg | 3 + .../flowy_icons/20x/type_formula.svg | 3 + .../resources/flowy_icons/20x/type_h1.svg | 3 + .../resources/flowy_icons/20x/type_h2.svg | 3 + .../resources/flowy_icons/20x/type_h3.svg | 3 + .../flowy_icons/20x/type_numbered_list.svg | 4 + .../resources/flowy_icons/20x/type_page.svg | 3 + .../resources/flowy_icons/20x/type_quote.svg | 3 + .../flowy_icons/20x/type_strikethrough.svg | 3 + .../resources/flowy_icons/20x/type_text.svg | 3 + .../resources/flowy_icons/20x/type_todo.svg | 3 + .../flowy_icons/20x/type_toggle_h1.svg | 3 + .../flowy_icons/20x/type_toggle_h2.svg | 3 + .../flowy_icons/20x/type_toggle_h3.svg | 3 + .../flowy_icons/20x/type_toggle_list.svg | 3 + frontend/resources/translations/en.json | 18 +- 77 files changed, 2233 insertions(+), 288 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart rename frontend/resources/flowy_icons/{24x => 16x}/calendar_layout.svg (100%) rename frontend/resources/flowy_icons/{24x => 16x}/close_filled.svg (100%) rename frontend/resources/flowy_icons/{24x => 16x}/database_layout.svg (100%) create mode 100644 frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_alignment.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_bold.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_check.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_inline_code.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_link.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_more.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_color.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_format.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_underline.svg create mode 100644 frontend/resources/flowy_icons/20x/type_bulleted_list.svg create mode 100644 frontend/resources/flowy_icons/20x/type_callout.svg create mode 100644 frontend/resources/flowy_icons/20x/type_font.svg create mode 100644 frontend/resources/flowy_icons/20x/type_formula.svg create mode 100644 frontend/resources/flowy_icons/20x/type_h1.svg create mode 100644 frontend/resources/flowy_icons/20x/type_h2.svg create mode 100644 frontend/resources/flowy_icons/20x/type_h3.svg create mode 100644 frontend/resources/flowy_icons/20x/type_numbered_list.svg create mode 100644 frontend/resources/flowy_icons/20x/type_page.svg create mode 100644 frontend/resources/flowy_icons/20x/type_quote.svg create mode 100644 frontend/resources/flowy_icons/20x/type_strikethrough.svg create mode 100644 frontend/resources/flowy_icons/20x/type_text.svg create mode 100644 frontend/resources/flowy_icons/20x/type_todo.svg create mode 100644 frontend/resources/flowy_icons/20x/type_toggle_h1.svg create mode 100644 frontend/resources/flowy_icons/20x/type_toggle_h2.svg create mode 100644 frontend/resources/flowy_icons/20x/type_toggle_h3.svg create mode 100644 frontend/resources/flowy_icons/20x/type_toggle_list.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index ea8db6fdad..1a8a3fcda8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -27,8 +27,9 @@ void main() { await tester.pumpAndSettle(); // click the align center - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); + await tester + .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m); // expect to see the align center final editorState = tester.editor.getCurrentEditorState(); @@ -36,13 +37,15 @@ void main() { expect(first.attributes[blockComponentAlign], 'center'); // click the align right - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); + await tester + .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m); expect(first.attributes[blockComponentAlign], 'right'); // click the align left - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); + await tester + .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m); expect(first.attributes[blockComponentAlign], 'left'); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart index c3a086626f..abfee17d8e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart @@ -1,5 +1,10 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -8,24 +13,33 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + Future selectText(WidgetTester tester, String text) async { + await tester.editor.updateSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: text.length, + ), + ); + } + + Future prepareForToolbar(WidgetTester tester, String text) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(); + + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(text); + await selectText(tester, text); + } + group('document toolbar:', () { testWidgets('font family', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - await tester.editor.tapLineOfEditorAt(0); - const text = 'font family'; - await tester.ime.insertText(text); - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text.length, - ), - ); + await prepareForToolbar(tester, 'font family'); + // tap more options button + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m); // tap the font family button final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey); await tester.tapButton(fontFamilyButton); @@ -46,5 +60,120 @@ void main() { abel, ); }); + + testWidgets('heading 1~3', (tester) async { + const text = 'heading'; + await prepareForToolbar(tester, text); + + Future testChangeHeading( + FlowySvgData svg, + String title, + int level, + ) async { + /// tap suggestions item + final suggestionsButton = find.byKey(kSuggestionsItemKey); + await tester.tapButton(suggestionsButton); + + /// tap item + await tester.ensureVisible(find.byFlowySvg(svg)); + await tester.tapButton(find.byFlowySvg(svg)); + + /// check the type of node is [HeadingBlockKeys.type] + await selectText(tester, text); + final editorState = tester.editor.getCurrentEditorState(); + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!, + nodeLevel = node.attributes[HeadingBlockKeys.level]!; + expect(node.type, HeadingBlockKeys.type); + expect(nodeLevel, level); + + /// show toolbar again + await selectText(tester, text); + + /// the text of suggestions item should be changed + expect( + find.descendant(of: suggestionsButton, matching: find.text(title)), + findsOneWidget, + ); + } + + await testChangeHeading( + FlowySvgs.type_h1_m, + LocaleKeys.document_toolbar_h1.tr(), + 1, + ); + + await testChangeHeading( + FlowySvgs.type_h2_m, + LocaleKeys.document_toolbar_h2.tr(), + 2, + ); + await testChangeHeading( + FlowySvgs.type_h3_m, + LocaleKeys.document_toolbar_h3.tr(), + 3, + ); + }); + + testWidgets('toggle 1~3', (tester) async { + const text = 'toggle'; + await prepareForToolbar(tester, text); + + Future testChangeToggle( + FlowySvgData svg, + String title, + int? level, + ) async { + /// tap suggestions item + final suggestionsButton = find.byKey(kSuggestionsItemKey); + await tester.tapButton(suggestionsButton); + + /// tap item + await tester.ensureVisible(find.byFlowySvg(svg)); + await tester.tapButton(find.byFlowySvg(svg)); + + /// check the type of node is [HeadingBlockKeys.type] + await selectText(tester, text); + final editorState = tester.editor.getCurrentEditorState(); + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!, + nodeLevel = node.attributes[ToggleListBlockKeys.level]; + expect(node.type, ToggleListBlockKeys.type); + expect(nodeLevel, level); + + /// show toolbar again + await selectText(tester, text); + + /// the text of suggestions item should be changed + expect( + find.descendant(of: suggestionsButton, matching: find.text(title)), + findsOneWidget, + ); + } + + await testChangeToggle( + FlowySvgs.type_toggle_list_m, + LocaleKeys.editor_toggleListShortForm.tr(), + null, + ); + + await testChangeToggle( + FlowySvgs.type_toggle_h1_m, + LocaleKeys.editor_toggleHeading1ShortForm.tr(), + 1, + ); + + await testChangeToggle( + FlowySvgs.type_toggle_h2_m, + LocaleKeys.editor_toggleHeading2ShortForm.tr(), + 2, + ); + + await testChangeToggle( + FlowySvgs.type_toggle_h3_m, + LocaleKeys.editor_toggleHeading3ShortForm.tr(), + 3, + ); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index 906b5ab69c..c29de9f72f 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -33,9 +34,15 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); + // tap the more options button + final moreOptionButton = find.findFlowyTooltip( + LocaleKeys.document_toolbar_moreOptions.tr(), + ); + await tester.tapButton(moreOptionButton); + // tap the inline math equation button - final inlineMathEquationButton = find.findFlowyTooltip( - LocaleKeys.document_plugins_createInlineMathEquation.tr(), + final inlineMathEquationButton = find.text( + LocaleKeys.editor_mathEquationShortForm.tr(), ); await tester.tapButton(inlineMathEquationButton); @@ -78,10 +85,15 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the inline math equation button - var inlineMathEquationButton = find.findFlowyTooltip( - LocaleKeys.document_plugins_createInlineMathEquation.tr(), + // tap the more options button + final moreOptionButton = find.findFlowyTooltip( + LocaleKeys.document_toolbar_moreOptions.tr(), ); + await tester.tapButton(moreOptionButton); + + // tap the inline math equation button + final inlineMathEquationButton = + find.byFlowySvg(FlowySvgs.type_formula_m); await tester.tapButton(inlineMathEquationButton); // expect to see the math equation block @@ -93,15 +105,10 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: 1), ); + await tester.tapButton(moreOptionButton); // expect to the see the inline math equation button is highlighted - inlineMathEquationButton = find.descendant( - of: find.findFlowyTooltip( - LocaleKeys.document_plugins_createInlineMathEquation.tr(), - ), - matching: find.byType(SVGIconItemWidget), - ); expect( - tester.widget(inlineMathEquationButton).isHighlight, + tester.widget(inlineMathEquationButton).color != null, isTrue, ); @@ -134,10 +141,15 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the inline math equation button - final inlineMathEquationButton = find.findFlowyTooltip( - LocaleKeys.document_plugins_createInlineMathEquation.tr(), + // tap the more options button + final moreOptionButton = find.findFlowyTooltip( + LocaleKeys.document_toolbar_moreOptions.tr(), ); + await tester.tapButton(moreOptionButton); + + // tap the inline math equation button + final inlineMathEquationButton = + find.byFlowySvg(FlowySvgs.type_formula_m); await tester.tapButton(inlineMathEquationButton); // expect to see the math equation block diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart index 8eb47fc15f..c4aa289855 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -85,16 +86,10 @@ void main() { ), ); - await tester.tapButton(find.byType(HeadingPopup)); - await tester.pumpAndSettle(); - - expect( - find.byType(HeadingButton), - findsNWidgets(3), - ); + await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m)); // tap the H1 button - await tester.tapButton(find.byType(HeadingButton).at(0)); + await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0)); await tester.pumpAndSettle(); final editorState = tester.editor.getCurrentEditorState(); diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 5f27305fe5..9a82c881e0 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1,17 +1,9 @@ import 'dart:io'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; +import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; @@ -27,10 +19,11 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; @@ -44,6 +37,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; +import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; @@ -76,6 +70,8 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; @@ -90,6 +86,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; // Non-exported member of the table_calendar library @@ -1572,7 +1571,7 @@ extension AppFlowyDatabaseTest on WidgetTester { of: textField, matching: find.byWidgetPredicate( (widget) => - widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m, + widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 77a26a9c58..386be9cc15 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -847,7 +847,7 @@ class _BoardTrailingState extends State { suffixIcon: Padding( padding: const EdgeInsets.only(left: 4, bottom: 8.0), child: FlowyIconButton( - icon: const FlowySvg(FlowySvgs.close_filled_m), + icon: const FlowySvg(FlowySvgs.close_filled_s), hoverColor: Colors.transparent, onPressed: () => _textController.clear(), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart index 5f40959c02..c7bc286371 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart @@ -24,11 +24,11 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { case DatabaseSettingAction.showProperties: return FlowySvgs.multiselect_s; case DatabaseSettingAction.showLayout: - return FlowySvgs.database_layout_m; + return FlowySvgs.database_layout_s; case DatabaseSettingAction.showGroup: return FlowySvgs.group_s; case DatabaseSettingAction.showCalendarLayout: - return FlowySvgs.calendar_layout_m; + return FlowySvgs.calendar_layout_s; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 97cceb1aa8..4ed434cfa1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -22,11 +22,21 @@ import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart'; +import 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/text_heading_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; + /// Wrapper for the appflowy editor. class AppFlowyEditorPage extends StatefulWidget { const AppFlowyEditorPage({ @@ -82,23 +92,17 @@ class _AppFlowyEditorPageState extends State ]; final List toolbarItems = [ - improveWritingItem..isActive = onlyShowInTextTypeAndExcludeTable, - aiWriterItem..isActive = onlyShowInTextTypeAndExcludeTable, - paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - headingsToolbarItem - ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - ...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType), - quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - bulletedListItem - ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - numberedListItem - ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - inlineMathEquationItem, - linkItem, - alignToolbarItem, - buildTextColorItem()..isActive = showInAnyTextType, - buildHighlightColorItem()..isActive = showInAnyTextType, - customizeFontToolbarItem..isActive = showInAnyTextType, + improveWritingItem, + aiWriterItem, + customTextHeadingItem, + ...customMarkdownFormatItems, + customTextColorItem, + customHighlightColorItem, + customInlineCodeItem, + suggestionsItem, + customLinkItem, + customTextAlignItem, + moreOptionItem, ]; List get characterShortcutEvents { @@ -410,6 +414,7 @@ class _AppFlowyEditorPageState extends State anchor: anchor, closeToolbar: closeToolbar, ), + floatingToolbarHeight: 32, child: editor, ), ); @@ -417,8 +422,16 @@ class _AppFlowyEditorPageState extends State return Center( child: FloatingToolbar( - style: styleCustomizer.floatingToolbarStyleBuilder(), + floatingToolbarHeight: 40, + padding: EdgeInsets.symmetric(horizontal: 6), + style: FloatingToolbarStyle( + backgroundColor: Theme.of(context).cardColor, + toolbarElevation: 10, + ), items: toolbarItems, + decoration: context.getPopoverDecoration( + borderRadius: BorderRadius.circular(6), + ), editorState: editorState, editorScrollController: editorScrollController, textDirection: textDirection, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index f7aa398046..3f8307bf13 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -17,7 +18,7 @@ const _aiWriterToolbarItemId = 'appflowy.editor.ai_writer'; final ToolbarItem improveWritingItem = ToolbarItem( id: _improveWritingToolbarItemId, group: 0, - isActive: onlyShowInSingleSelectionAndTextType, + isActive: onlyShowInTextTypeAndExcludeTable, builder: (context, editorState, _, __, tooltipBuilder) => ImproveWritingButton( editorState: editorState, @@ -28,7 +29,7 @@ final ToolbarItem improveWritingItem = ToolbarItem( final ToolbarItem aiWriterItem = ToolbarItem( id: _aiWriterToolbarItemId, group: 0, - isActive: onlyShowInSingleSelectionAndTextType, + isActive: onlyShowInTextTypeAndExcludeTable, builder: (context, editorState, _, __, tooltipBuilder) => AiWriterToolbarActionList( editorState: editorState, @@ -53,6 +54,7 @@ class AiWriterToolbarActionList extends StatefulWidget { class _AiWriterToolbarActionListState extends State { final popoverController = PopoverController(); + bool isSelected = false; @override Widget build(BuildContext context) { @@ -60,11 +62,15 @@ class _AiWriterToolbarActionListState extends State { controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(-8.0, 2.0), - margin: const EdgeInsets.all(8.0), onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, popupBuilder: (context) => buildPopoverContent(), - child: buildChild(), + child: buildChild(context), ); } @@ -87,12 +93,11 @@ class _AiWriterToolbarActionListState extends State { Widget actionWrapper(AiWriterCommand command) { return SizedBox( - height: 30.0, + height: 36, child: FlowyButton( - leftIcon: FlowySvg( - command.icon, - size: const Size.square(16), - ), + leftIconSize: const Size.square(20), + leftIcon: FlowySvg(command.icon), + iconPadding: 12, text: FlowyText( command.i18n, figmaLineHeight: 20, @@ -112,33 +117,35 @@ class _AiWriterToolbarActionListState extends State { ); } - Widget buildChild() { + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; final child = FlowyIconButton( - hoverColor: Colors.transparent, - width: 36, - height: 24, + width: 52, + height: 32, + isSelected: isSelected, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), icon: Row( + mainAxisSize: MainAxisSize.min, children: [ - const FlowySvg( - FlowySvgs.ai_sparks_s, - size: Size.square(16.0), - color: Color(0xFFD08EED), + FlowySvg( + FlowySvgs.toolbar_ai_writer_m, + size: Size.square(20), + color: iconColor, ), - const FlowySvg( - FlowySvgs.ai_source_drop_down_s, - size: Size.square(12), - color: Color(0xFF8F959E), + FlowySvg( + FlowySvgs.toolbar_arrow_down_m, + size: Size.square(20), + color: iconColor, ), ], ), - iconPadding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), onPressed: () { if (_isAIEnabled(widget.editorState)) { keepEditorFocusNotifier.increase(); popoverController.show(); + setState(() { + isSelected = true; + }); } else { showToastNotification( context, @@ -173,16 +180,13 @@ class ImproveWritingButton extends StatelessWidget { @override Widget build(BuildContext context) { final child = FlowyIconButton( - hoverColor: Colors.transparent, - width: 24, - icon: const FlowySvg( - FlowySvgs.ai_improve_writing_s, - size: Size.square(16.0), - color: Color(0xFFD08EED), - ), - iconPadding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, + width: 36, + height: 32, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_ai_improve_writing_m, + size: Size.square(20.0), + color: Theme.of(context).iconTheme.color, ), onPressed: () { if (_isAIEnabled(editorState)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart index aed78172b2..73241382dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart @@ -1,4 +1,6 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; bool _isTableType(String type) { @@ -32,3 +34,25 @@ bool onlyShowInSingleTextTypeSelectionAndExcludeTable( return onlyShowInSingleSelectionAndTextType(editorState) && notShowInTable(editorState); } + +bool enableSuggestions( + EditorState editorState, +) { + final selection = editorState.selection; + if (selection == null || !selection.isSingle) { + return false; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return false; + } + return (node.delta != null && suggestionsItemTypes.contains(node.type)) && + notShowInTable(editorState); +} + +final Set suggestionsItemTypes = { + ...toolbarItemWhiteList, + ToggleListBlockKeys.type, + TodoListBlockKeys.type, + CalloutBlockKeys.type, +}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart index d297328681..e0f63e57c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart @@ -9,8 +9,6 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -21,68 +19,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; -const kFontToolbarItemId = 'editor.font'; - -@visibleForTesting -const kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem'); - -final customizeFontToolbarItem = ToolbarItem( - id: kFontToolbarItemId, - group: 4, - isActive: onlyShowInTextType, - builder: (context, editorState, highlightColor, _, tooltipBuilder) { - final selection = editorState.selection!; - final popoverController = PopoverController(); - final String? currentFontFamily = editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); - - Widget child = FontFamilyDropDown( - currentFontFamily: currentFontFamily ?? '', - offset: const Offset(0, 12), - popoverController: popoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - onFontFamilyChanged: (fontFamily) async { - popoverController.close(); - try { - await editorState.formatDelta(selection, { - AppFlowyRichTextKeys.fontFamily: fontFamily, - }); - } catch (e) { - Log.error('Failed to set font family: $e'); - } - }, - onResetFont: () async { - popoverController.close(); - await editorState - .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); - }, - child: FlowyButton( - key: kFontFamilyToolbarItemKey, - useIntrinsicWidth: true, - hoverColor: Colors.grey.withValues(alpha: 0.3), - onTap: () => popoverController.show(), - text: const FlowySvg( - FlowySvgs.font_family_s, - size: Size.square(16.0), - color: Colors.white, - ), - ), - ); - - if (tooltipBuilder != null) { - child = tooltipBuilder( - context, - kFontToolbarItemId, - LocaleKeys.document_plugins_fonts.tr(), - child, - ); - } - - return child; - }, -); - class ThemeFontFamilySetting extends StatefulWidget { const ThemeFontFamilySetting({ super.key, @@ -163,6 +99,11 @@ class _FontFamilyDropDownState extends State { popoverKey: ThemeFontFamilySetting.popoverKey, popoverController: widget.popoverController, currentValue: currentValue, + margin: EdgeInsets.zero, + boxConstraints: const BoxConstraints( + maxWidth: 240, + maxHeight: 420, + ), onClose: () { query.value = ''; widget.onClose?.call(); @@ -171,27 +112,25 @@ class _FontFamilyDropDownState extends State { child: widget.child, popupBuilder: (_) { widget.onOpen?.call(); - return CustomScrollView( - shrinkWrap: true, - slivers: [ - SliverPadding( - padding: const EdgeInsets.only(right: 8), - sliver: SliverToBoxAdapter( - child: FlowyTextField( - key: ThemeFontFamilySetting.textFieldKey, - hintText: - LocaleKeys.settings_appearance_fontFamily_search.tr(), - autoFocus: false, - debounceDuration: const Duration(milliseconds: 300), - onChanged: (value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyTextField( + key: ThemeFontFamilySetting.textFieldKey, + hintText: LocaleKeys.settings_appearance_fontFamily_search.tr(), + autoFocus: false, + debounceDuration: const Duration(milliseconds: 300), + onChanged: (value) { + setState(() { query.value = value; - }, - ), + }); + }, ), ), - const SliverToBoxAdapter( - child: SizedBox(height: 4), - ), + Container(height: 1, color: Theme.of(context).dividerColor), ValueListenableBuilder( valueListenable: query, builder: (context, value, child) { @@ -206,14 +145,32 @@ class _FontFamilyDropDownState extends State { .sorted((a, b) => levenshtein(a, b)) .toList(); } - return SliverFixedExtentList.builder( - itemBuilder: (context, index) => _fontFamilyItemButton( - context, - getGoogleFontSafely(displayed[index]), - ), - itemCount: displayed.length, - itemExtent: 32, - ); + return displayed.length >= 10 + ? Flexible( + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemBuilder: (context, index) => + _fontFamilyItemButton( + context, + getGoogleFontSafely(displayed[index]), + ), + itemCount: displayed.length, + ), + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: List.generate( + displayed.length, + (index) => _fontFamilyItemButton( + context, + getGoogleFontSafely(displayed[index]), + ), + ), + ), + ); }, ), ], @@ -233,16 +190,18 @@ class _FontFamilyDropDownState extends State { waitDuration: const Duration(milliseconds: 150), child: SizedBox( key: ValueKey(buttonFontFamily), - height: 32, + height: 36, child: FlowyButton( onHover: (_) => FocusScope.of(context).unfocus(), - text: FlowyText.medium( + text: FlowyText( buttonFontFamily.fontFamilyDisplayName, fontFamily: buttonFontFamily, + figmaLineHeight: 20, + fontWeight: FontWeight.w400, ), rightIcon: buttonFontFamily == widget.currentFontFamily.parseFontFamilyName() - ? const FlowySvg(FlowySvgs.check_s) + ? const FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () { if (widget.onFontFamilyChanged != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart index 061a6fe320..cd3779cb6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart @@ -9,7 +9,7 @@ const _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation'; final ToolbarItem inlineMathEquationItem = ToolbarItem( id: _kInlineMathEquationToolbarItemId, - group: 2, + group: 4, isActive: onlyShowInSingleSelectionAndTextType, builder: (context, editorState, highlightColor, _, tooltipBuilder) { final selection = editorState.selection!; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart new file mode 100644 index 0000000000..68d439ddfe --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -0,0 +1,89 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +final List customMarkdownFormatItems = [ + _FormatToolbarItem( + id: 'bold', + name: 'bold', + svg: FlowySvgs.toolbar_bold_m, + ), + _FormatToolbarItem( + id: 'underline', + name: 'underline', + svg: FlowySvgs.toolbar_underline_m, + ), + _FormatToolbarItem( + id: 'italic', + name: 'italic', + svg: FlowySvgs.toolbar_inline_italic_m, + ), +]; + +final ToolbarItem customInlineCodeItem = _FormatToolbarItem( + id: 'code', + name: 'code', + svg: FlowySvgs.toolbar_inline_code_m, + group: 2, +); + +class _FormatToolbarItem extends ToolbarItem { + _FormatToolbarItem({ + required String id, + required String name, + required FlowySvgData svg, + super.group = 1, + }) : super( + id: 'editor.$id', + isActive: showInAnyTextType, + builder: ( + context, + editorState, + highlightColor, + iconColor, + tooltipBuilder, + ) { + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.allSatisfyInSelection( + selection, + (delta) => + delta.isNotEmpty && + delta.everyAttributes((attr) => attr[name] == true), + ); + + final hoverColor = Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).hoverColor + : AFThemeExtension.of(context).toolbarHoverColor; + + final child = FlowyIconButton( + width: 36, + height: 32, + hoverColor: hoverColor, + icon: FlowySvg( + svg, + size: Size.square(20.0), + color: isHighlight + ? highlightColor + : Theme.of(context).iconTheme.color, + ), + onPressed: () => editorState.toggleAttribute(name), + ); + + if (tooltipBuilder != null) { + return tooltipBuilder( + context, + id, + getTooltipText(id), + child, + ); + } + return child; + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart new file mode 100644 index 0000000000..53cc9ce4ca --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -0,0 +1,75 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +const _kHighlightColorItemId = 'editor.highlightColor'; + +final customHighlightColorItem = ToolbarItem( + id: _kHighlightColorItemId, + group: 1, + isActive: showInAnyTextType, + builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { + String? highlightColorHex; + + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { + if (delta.everyAttributes((attr) => attr.isEmpty)) { + return false; + } + + return delta.everyAttributes((attributes) { + highlightColorHex = attributes[AppFlowyRichTextKeys.backgroundColor]; + return highlightColorHex != null; + }); + }); + + final child = FlowyIconButton( + width: 36, + height: 32, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_text_highlight_m, + size: Size.square(20.0), + color: isHighlight ? highlightColor : Theme.of(context).iconTheme.color, + ), + onPressed: () { + bool showClearButton = false; + nodes.allSatisfyInSelection(selection, (delta) { + if (!showClearButton) { + showClearButton = delta.whereType().any( + (element) { + return element + .attributes?[AppFlowyRichTextKeys.backgroundColor] != + null; + }, + ); + } + return true; + }); + showColorMenu( + context, + editorState, + selection, + currentColorHex: highlightColorHex, + isTextColor: false, + showClearButton: showClearButton, + ); + }, + ); + + if (tooltipBuilder != null) { + return tooltipBuilder( + context, + _kHighlightColorItemId, + AppFlowyEditorL10n.current.highlightColor, + child, + ); + } + + return child; + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart new file mode 100644 index 0000000000..048e330f70 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -0,0 +1,45 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +const _kLinkItemId = 'editor.link'; + +final customLinkItem = ToolbarItem( + id: _kLinkItemId, + group: 4, + isActive: onlyShowInSingleSelectionAndTextType, + builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + final isHref = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[AppFlowyRichTextKeys.href] != null, + ); + }); + + final child = FlowyIconButton( + width: 36, + height: 32, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_link_m, + size: Size.square(20.0), + color: isHref ? highlightColor : Theme.of(context).iconTheme.color, + ), + onPressed: () => showLinkMenu(context, editorState, selection, isHref), + ); + + if (tooltipBuilder != null) { + return tooltipBuilder( + context, + _kLinkItemId, + AppFlowyEditorL10n.current.link, + child, + ); + } + + return child; + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart new file mode 100644 index 0000000000..a5f4d59559 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -0,0 +1,198 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.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'; + +const _kTextAlignItemId = 'editor.text_align'; + +final ToolbarItem customTextAlignItem = ToolbarItem( + id: _kTextAlignItemId, + group: 4, + isActive: onlyShowInSingleSelectionAndTextType, + builder: ( + context, + editorState, + highlightColor, + iconColor, + tooltipBuilder, + ) { + return TextAlignActionList( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + highlightColor: highlightColor, + ); + }, +); + +class TextAlignActionList extends StatefulWidget { + const TextAlignActionList({ + super.key, + required this.editorState, + required this.highlightColor, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + final Color highlightColor; + + @override + State createState() => _TextAlignActionListState(); +} + +class _TextAlignActionListState extends State { + final popoverController = PopoverController(); + + bool isSelected = false; + + EditorState get editorState => widget.editorState; + + Color get highlightColor => widget.highlightColor; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(-8.0, 2.0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(context), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; + final child = FlowyIconButton( + width: 52, + height: 32, + isSelected: isSelected, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.toolbar_alignment_m, + size: Size.square(20), + color: iconColor, + ), + FlowySvg( + FlowySvgs.toolbar_arrow_down_m, + size: Size.square(20), + color: iconColor, + ), + ], + ), + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + ); + + return widget.tooltipBuilder?.call( + context, + _kTextAlignItemId, + LocaleKeys.document_toolbar_textAlign.tr(), + child, + ) ?? + child; + } + + Widget buildPopoverContent() { + return MouseRegion( + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(4.0), + children: List.generate(TextAlignCommand.values.length, (index) { + final command = TextAlignCommand.values[index]; + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.every( + (n) => n.attributes[blockComponentAlign] == command.name, + ); + final color = + isHighlight ? highlightColor : Theme.of(context).iconTheme.color; + + return SizedBox( + height: 36, + child: FlowyButton( + leftIconSize: const Size.square(20), + leftIcon: FlowySvg( + command.svg, + color: color, + ), + iconPadding: 12, + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + color: color, + ), + onTap: () { + command.onAlignChanged(editorState); + popoverController.close(); + }, + ), + ); + }), + ), + ); + } +} + +enum TextAlignCommand { + left(FlowySvgs.toolbar_text_align_left_m), + center(FlowySvgs.toolbar_text_align_center_m), + right(FlowySvgs.toolbar_text_align_right_m); + + const TextAlignCommand(this.svg); + + final FlowySvgData svg; + + String get title { + switch (this) { + case left: + return LocaleKeys.document_toolbar_alignLeft.tr(); + case center: + return LocaleKeys.document_toolbar_alignCenter.tr(); + case right: + return LocaleKeys.document_toolbar_alignRight.tr(); + } + } + + Future onAlignChanged(EditorState editorState) async { + final selection = editorState.selection!; + + await editorState.updateNode( + selection, + (node) => node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: name, + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart new file mode 100644 index 0000000000..7cb7f47bfb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -0,0 +1,76 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +const _kTextColorItemId = 'editor.textColor'; + +final customTextColorItem = ToolbarItem( + id: _kTextColorItemId, + group: 1, + isActive: showInAnyTextType, + builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { + String? textColorHex; + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { + if (delta.everyAttributes((attr) => attr.isEmpty)) { + return false; + } + + return delta.everyAttributes((attr) { + textColorHex = attr[AppFlowyRichTextKeys.textColor]; + return (textColorHex != null); + }); + }); + + final child = FlowyIconButton( + width: 36, + height: 32, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_text_color_m, + size: Size.square(20.0), + color: isHighlight ? highlightColor : Theme.of(context).iconTheme.color, + ), + onPressed: () { + bool showClearButton = false; + nodes.allSatisfyInSelection( + selection, + (delta) { + if (!showClearButton) { + showClearButton = delta.whereType().any( + (element) { + return element.attributes?[AppFlowyRichTextKeys.textColor] != + null; + }, + ); + } + return true; + }, + ); + showColorMenu( + context, + editorState, + selection, + currentColorHex: textColorHex, + isTextColor: true, + showClearButton: showClearButton, + ); + }, + ); + + if (tooltipBuilder != null) { + return tooltipBuilder( + context, + _kTextColorItemId, + AppFlowyEditorL10n.current.textColor, + child, + ); + } + + return child; + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart new file mode 100644 index 0000000000..7a3e072246 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -0,0 +1,324 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_backend/log.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'; + +import '../../editor_page.dart'; + +const _kMoreOptionItemId = 'editor.more_option'; +const kFontToolbarItemId = 'editor.font'; + +@visibleForTesting +const kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem'); + +final ToolbarItem moreOptionItem = ToolbarItem( + id: _kMoreOptionItemId, + group: 5, + isActive: showInAnyTextType, + builder: ( + context, + editorState, + highlightColor, + iconColor, + tooltipBuilder, + ) { + return MoreOptionActionList( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + highlightColor: highlightColor, + ); + }, +); + +class MoreOptionActionList extends StatefulWidget { + const MoreOptionActionList({ + super.key, + required this.editorState, + required this.highlightColor, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + final Color highlightColor; + + @override + State createState() => _MoreOptionActionListState(); +} + +class _MoreOptionActionListState extends State { + final popoverController = PopoverController(); + final fontPopoverController = PopoverController(); + + bool isSelected = false; + + EditorState get editorState => widget.editorState; + + Color get highlightColor => widget.highlightColor; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + fontPopoverController.close(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(-8.0, 2.0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(context), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; + final child = FlowyIconButton( + width: 36, + height: 32, + isSelected: isSelected, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_more_m, + size: Size.square(20), + color: iconColor, + ), + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + ); + + return widget.tooltipBuilder?.call( + context, + _kMoreOptionItemId, + LocaleKeys.document_toolbar_moreOptions.tr(), + child, + ) ?? + child; + } + + Color? getFormulaColor() { + if (isFormulaHighlight(editorState)) { + return widget.highlightColor; + } + return null; + } + + Color? getStrikethroughColor() { + final selection = editorState.selection; + if (selection == null || selection.isCollapsed) { + return null; + } + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return null; + } + + final nodes = editorState.getNodesInSelection(selection); + final isHighlight = nodes.allSatisfyInSelection( + selection, + (delta) => + delta.isNotEmpty && + delta.everyAttributes( + (attr) => attr[MoreOptionCommand.strikethrough.name] == true, + ), + ); + return isHighlight ? widget.highlightColor : null; + } + + Widget buildPopoverContent() { + final showFormula = onlyShowInSingleSelectionAndTextType(editorState); + return MouseRegion( + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(4.0), + children: [ + buildFontSelector(), + buildCommandItem( + MoreOptionCommand.strikethrough, + getStrikethroughColor(), + ), + if (showFormula) + buildCommandItem( + MoreOptionCommand.formula, + getFormulaColor(), + ), + ], + ), + ); + } + + Widget buildCommandItem(MoreOptionCommand command, Color? color) { + return SizedBox( + height: 36, + child: FlowyButton( + key: command == MoreOptionCommand.font + ? kFontFamilyToolbarItemKey + : null, + leftIconSize: const Size.square(20), + leftIcon: FlowySvg( + command.svg, + color: color, + ), + iconPadding: 12, + text: FlowyText( + command.title, + figmaLineHeight: 20, + fontWeight: FontWeight.w400, + color: color, + ), + onTap: () { + command.onExecute(editorState); + if (command != MoreOptionCommand.font) { + popoverController.close(); + } + }, + ), + ); + } + + Widget buildFontSelector() { + final selection = editorState.selection!; + final String? currentFontFamily = editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); + return FontFamilyDropDown( + currentFontFamily: currentFontFamily ?? '', + offset: const Offset(-240, 0), + popoverController: fontPopoverController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + onFontFamilyChanged: (fontFamily) async { + fontPopoverController.close(); + popoverController.close(); + try { + await editorState.formatDelta(selection, { + AppFlowyRichTextKeys.fontFamily: fontFamily, + }); + } catch (e) { + Log.error('Failed to set font family: $e'); + } + }, + onResetFont: () async { + fontPopoverController.close(); + popoverController.close(); + await editorState + .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); + }, + child: buildCommandItem(MoreOptionCommand.font, null), + ); + } +} + +enum MoreOptionCommand { + font(FlowySvgs.type_font_m), + strikethrough(FlowySvgs.type_strikethrough_m), + formula(FlowySvgs.type_formula_m); + + const MoreOptionCommand(this.svg); + + final FlowySvgData svg; + + String get title { + switch (this) { + case font: + return LocaleKeys.document_toolbar_font.tr(); + case strikethrough: + return LocaleKeys.editor_strikethrough.tr(); + case formula: + return LocaleKeys.editor_mathEquationShortForm.tr(); + } + } + + Future onExecute(EditorState editorState) async { + if (this == strikethrough) { + await editorState.toggleAttribute(name); + } else if (this == formula) { + final selection = editorState.selection; + if (selection == null || selection.isCollapsed) { + return; + } + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + + final transaction = editorState.transaction; + final isHighlight = isFormulaHighlight(editorState); + if (isHighlight) { + final formula = delta + .slice(selection.startIndex, selection.endIndex) + .whereType() + .firstOrNull + ?.attributes?[InlineMathEquationKeys.formula]; + assert(formula != null); + if (formula == null) { + return; + } + // clear the format + transaction.replaceText( + node, + selection.startIndex, + selection.length, + formula, + attributes: {}, + ); + } else { + final text = editorState.getTextInSelection(selection).join(); + transaction.replaceText( + node, + selection.startIndex, + selection.length, + MentionBlockKeys.mentionChar, + attributes: { + InlineMathEquationKeys.formula: text, + }, + ); + } + await editorState.apply(transaction); + } + } +} + +bool isFormulaHighlight(EditorState editorState) { + final selection = editorState.selection; + if (selection == null || selection.isCollapsed) { + return false; + } + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return false; + } + + final nodes = editorState.getNodesInSelection(selection); + return nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[InlineMathEquationKeys.formula] != null, + ); + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart new file mode 100644 index 0000000000..5a4f5e2ce1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -0,0 +1,313 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.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'; + +const _kTextHeadingItemId = 'editor.text_heading'; + +final ToolbarItem customTextHeadingItem = ToolbarItem( + id: _kTextHeadingItemId, + group: 1, + isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, + builder: ( + context, + editorState, + highlightColor, + iconColor, + tooltipBuilder, + ) { + return TextHeadingActionList( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + ); + }, +); + +class TextHeadingActionList extends StatefulWidget { + const TextHeadingActionList({ + super.key, + required this.editorState, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + + @override + State createState() => _TextHeadingActionListState(); +} + +class _TextHeadingActionListState extends State { + final popoverController = PopoverController(); + + bool isSelected = false; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(-8.0, 2.0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(context), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; + final child = FlowyIconButton( + width: 52, + height: 32, + isSelected: isSelected, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.toolbar_text_format_m, + size: Size.square(20), + color: iconColor, + ), + FlowySvg( + FlowySvgs.toolbar_arrow_down_m, + size: Size.square(20), + color: iconColor, + ), + ], + ), + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + ); + + return widget.tooltipBuilder?.call( + context, + _kTextHeadingItemId, + LocaleKeys.document_toolbar_textSize.tr(), + child, + ) ?? + child; + } + + Widget buildPopoverContent() { + return MouseRegion( + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(4.0), + children: List.generate(TextHeadingCommand.values.length, (index) { + final command = TextHeadingCommand.values[index]; + return SizedBox( + height: 36, + child: FlowyButton( + leftIconSize: const Size.square(20), + leftIcon: FlowySvg(command.svg), + iconPadding: 12, + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () { + command.onExecute(widget.editorState); + popoverController.close(); + }, + ), + ); + }), + ), + ); + } +} + +enum TextHeadingCommand { + text(FlowySvgs.type_text_m), + h1(FlowySvgs.type_h1_m), + h2(FlowySvgs.type_h2_m), + h3(FlowySvgs.type_h3_m); + + const TextHeadingCommand(this.svg); + + final FlowySvgData svg; + + String get title { + switch (this) { + case text: + return AppFlowyEditorL10n.current.text; + case h1: + return LocaleKeys.document_toolbar_h1.tr(); + case h2: + return LocaleKeys.document_toolbar_h2.tr(); + case h3: + return LocaleKeys.document_toolbar_h3.tr(); + } + } + + void onExecute(EditorState editorState) { + switch (this) { + case text: + formatNodeToText(editorState); + break; + case h1: + onHeadingLevelChanged(editorState, 1); + break; + case h2: + onHeadingLevelChanged(editorState, 2); + break; + case h3: + onHeadingLevelChanged(editorState, 3); + break; + } + } +} + +void formatNodeToText(EditorState editorState) { + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!; + final delta = (node.delta ?? Delta()).toJson(); + editorState.formatNode( + selection, + (node) => node.copyWith( + type: ParagraphBlockKeys.type, + attributes: { + blockComponentDelta: delta, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + }, + ), + ); +} + +Future onHeadingLevelChanged( + EditorState editorState, + int newLevel, +) async { + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!; + final delta = (node.delta ?? Delta()).toJson(); + final level = node.attributes[HeadingBlockKeys.level] ?? 1; + final originLevel = level; + final type = newLevel == originLevel && node.type == HeadingBlockKeys.type + ? ParagraphBlockKeys.type + : HeadingBlockKeys.type; + + if (type == HeadingBlockKeys.type) { + // from paragraph to heading + final newNode = node.copyWith( + type: type, + attributes: { + HeadingBlockKeys.level: newLevel, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: delta, + }, + ); + final children = node.children.map((child) => child.deepCopy()); + + final transaction = editorState.transaction; + transaction.insertNodes( + selection.start.path.next, + [newNode, ...children], + ); + transaction.deleteNode(node); + await editorState.apply(transaction); + } else { + // from heading to paragraph + await editorState.formatNode( + selection, + (node) => node.copyWith( + type: type, + attributes: { + HeadingBlockKeys.level: newLevel, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: delta, + }, + ), + ); + } +} + +Future onToggleLevelChanged( + EditorState editorState, + int? newLevel, +) async { + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!; + final delta = (node.delta ?? Delta()).toJson(); + final level = node.attributes[ToggleListBlockKeys.level]; + final originLevel = level; + + final type = newLevel == originLevel && node.type == ToggleListBlockKeys.type + ? ParagraphBlockKeys.type + : ToggleListBlockKeys.type; + + if (type == ToggleListBlockKeys.type) { + // from paragraph to heading + final newNode = node.copyWith( + type: type, + attributes: { + if (newLevel != null) ToggleListBlockKeys.level: newLevel, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: delta, + }, + ); + final children = node.children.map((child) => child.deepCopy()); + + final transaction = editorState.transaction; + transaction.insertNodes( + selection.start.path.next, + [newNode, ...children], + ); + transaction.deleteNode(node); + await editorState.apply(transaction); + } else { + // from heading to paragraph + await editorState.formatNode( + selection, + (node) => node.copyWith( + type: type, + attributes: { + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: delta, + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart new file mode 100644 index 0000000000..946d76b9ab --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -0,0 +1,479 @@ +import 'dart:collection'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +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:flutter/material.dart'; + +import 'text_heading_toolbar_item.dart'; + +const _kSuggestionsItemId = 'editor.suggestions'; + +@visibleForTesting +const kSuggestionsItemKey = ValueKey('SuggestionsItem'); + +@visibleForTesting +const kSuggestionsItemListKey = ValueKey('SuggestionsItemList'); + +final ToolbarItem suggestionsItem = ToolbarItem( + id: _kSuggestionsItemId, + group: 3, + isActive: enableSuggestions, + builder: ( + context, + editorState, + highlightColor, + iconColor, + tooltipBuilder, + ) { + return SuggestionsActionList( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + ); + }, +); + +class SuggestionsActionList extends StatefulWidget { + const SuggestionsActionList({ + super.key, + required this.editorState, + this.tooltipBuilder, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + + @override + State createState() => _SuggestionsActionListState(); +} + +class _SuggestionsActionListState extends State { + final popoverController = PopoverController(); + + bool isSelected = false; + + final List suggestionItems = suggestions.sublist(0, 4); + final List turnIntoItems = + suggestions.sublist(4, suggestions.length); + + EditorState get editorState => widget.editorState; + + SuggestionItem currentSuggestionItem = textSuggestionItem; + + @override + void initState() { + super.initState(); + refreshSuggestions(); + } + + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(-8.0, 2.0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400), + popupBuilder: (context) => buildPopoverContent(context), + child: buildChild(context), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; + final child = FlowyHover( + isSelected: () => isSelected, + style: HoverStyle( + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + foregroundColorOnHover: Theme.of(context).iconTheme.color, + ), + resetHoverOnRebuild: false, + child: FlowyTooltip( + preferBelow: true, + child: RawMaterialButton( + key: kSuggestionsItemKey, + constraints: BoxConstraints(maxHeight: 32, minWidth: 60), + clipBehavior: Clip.antiAlias, + hoverElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder(borderRadius: Corners.s6Border), + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + elevation: 0, + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + currentSuggestionItem.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + FlowySvg( + FlowySvgs.toolbar_arrow_down_m, + size: Size.square(20), + color: iconColor, + ), + ], + ), + ), + ), + ), + ); + + return widget.tooltipBuilder?.call( + context, + _kSuggestionsItemId, + currentSuggestionItem.title, + child, + ) ?? + child; + } + + Widget buildPopoverContent(BuildContext context) { + final textColor = Color(0xff99A1A8); + return MouseRegion( + child: SingleChildScrollView( + key: kSuggestionsItemListKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildSubTitle( + LocaleKeys.document_toolbar_suggestions.tr(), + textColor, + ), + ...List.generate(suggestionItems.length, (index) { + return buildItem(suggestionItems[index]); + }), + buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor), + ...List.generate(turnIntoItems.length, (index) { + return buildItem(turnIntoItems[index]); + }), + ], + ), + ), + ); + } + + Widget buildItem(SuggestionItem item) { + return SizedBox( + height: 36, + child: FlowyButton( + leftIconSize: const Size.square(20), + leftIcon: FlowySvg(item.svg), + iconPadding: 12, + text: FlowyText( + item.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () { + item.onTap(widget.editorState); + popoverController.close(); + }, + ), + ); + } + + Widget buildSubTitle(String text, Color color) { + return Container( + height: 32, + margin: EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.semibold( + text, + color: color, + figmaLineHeight: 16, + ), + ), + ); + } + + void refreshSuggestions() { + final selection = editorState.selection; + if (selection == null || !selection.isSingle) { + return; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null || node.delta == null) { + return; + } + final nodeType = node.type; + SuggestionType? suggestionType; + if (nodeType == HeadingBlockKeys.type) { + final level = node.attributes[HeadingBlockKeys.level] ?? 1; + if (level == 1) { + suggestionType = SuggestionType.h1; + } else if (level == 2) { + suggestionType = SuggestionType.h2; + } else if (level == 3) { + suggestionType = SuggestionType.h3; + } + } else if (nodeType == ToggleListBlockKeys.type) { + final level = node.attributes[ToggleListBlockKeys.level]; + if (level == null) { + suggestionType = SuggestionType.toggle; + } else if (level == 1) { + suggestionType = SuggestionType.toggleH1; + } else if (level == 2) { + suggestionType = SuggestionType.toggleH2; + } else if (level == 3) { + suggestionType = SuggestionType.toggleH3; + } + } else { + suggestionType = nodeType2SuggestionType[nodeType]; + } + if (suggestionType == null) return; + suggestionItems.clear(); + turnIntoItems.clear(); + for (final item in suggestions) { + if (item.type.group == suggestionType.group) { + suggestionItems.add(item); + } else { + turnIntoItems.add(item); + } + } + currentSuggestionItem = + suggestions.where((item) => item.type == suggestionType).first; + } +} + +class SuggestionItem { + SuggestionItem({ + required this.type, + required this.title, + required this.svg, + required this.onTap, + }); + + final SuggestionType type; + final String title; + final FlowySvgData svg; + final ValueChanged onTap; +} + +enum SuggestionGroup { textHeading, list, toggle, quote } + +enum SuggestionType { + text(SuggestionGroup.textHeading), + h1(SuggestionGroup.textHeading), + h2(SuggestionGroup.textHeading), + h3(SuggestionGroup.textHeading), + checkbox(SuggestionGroup.list), + bulleted(SuggestionGroup.list), + numbered(SuggestionGroup.list), + toggle(SuggestionGroup.toggle), + toggleH1(SuggestionGroup.toggle), + toggleH2(SuggestionGroup.toggle), + toggleH3(SuggestionGroup.toggle), + callOut(SuggestionGroup.quote), + quote(SuggestionGroup.quote); + + const SuggestionType(this.group); + + final SuggestionGroup group; +} + +final textSuggestionItem = SuggestionItem( + type: SuggestionType.text, + title: AppFlowyEditorL10n.current.text, + svg: FlowySvgs.type_text_m, + onTap: (state) => formatNodeToText(state), +); + +final h1SuggestionItem = SuggestionItem( + type: SuggestionType.h1, + title: LocaleKeys.document_toolbar_h1.tr(), + svg: FlowySvgs.type_h1_m, + onTap: (state) => onHeadingLevelChanged(state, 1), +); + +final h2SuggestionItem = SuggestionItem( + type: SuggestionType.h2, + title: LocaleKeys.document_toolbar_h2.tr(), + svg: FlowySvgs.type_h2_m, + onTap: (state) => onHeadingLevelChanged(state, 2), +); + +final h3SuggestionItem = SuggestionItem( + type: SuggestionType.h3, + title: LocaleKeys.document_toolbar_h3.tr(), + svg: FlowySvgs.type_h3_m, + onTap: (state) => onHeadingLevelChanged(state, 3), +); + +final checkboxSuggestionItem = SuggestionItem( + type: SuggestionType.checkbox, + title: LocaleKeys.editor_checkbox.tr(), + svg: FlowySvgs.type_todo_m, + onTap: (state) { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + final isHighlight = node.type == TodoListBlockKeys.type; + state.formatNode( + selection, + (node) => node.copyWith( + type: isHighlight ? ParagraphBlockKeys.type : TodoListBlockKeys.type, + ), + ); + }, +); + +final bulletedSuggestionItem = SuggestionItem( + type: SuggestionType.bulleted, + title: LocaleKeys.editor_bulletedListShortForm.tr(), + svg: FlowySvgs.type_bulleted_list_m, + onTap: (state) { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + final isHighlight = node.type == BulletedListBlockKeys.type; + state.formatNode( + selection, + (node) => node.copyWith( + type: + isHighlight ? ParagraphBlockKeys.type : BulletedListBlockKeys.type, + ), + ); + }, +); + +final numberedSuggestionItem = SuggestionItem( + type: SuggestionType.numbered, + title: LocaleKeys.editor_numberedListShortForm.tr(), + svg: FlowySvgs.type_numbered_list_m, + onTap: (state) { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + final isHighlight = node.type == NumberedListBlockKeys.type; + state.formatNode( + selection, + (node) => node.copyWith( + type: + isHighlight ? ParagraphBlockKeys.type : NumberedListBlockKeys.type, + ), + ); + }, +); + +final toggleSuggestionItem = SuggestionItem( + type: SuggestionType.toggle, + title: LocaleKeys.editor_toggleListShortForm.tr(), + svg: FlowySvgs.type_toggle_list_m, + onTap: (state) => onToggleLevelChanged(state, null), +); + +final toggleH1SuggestionItem = SuggestionItem( + type: SuggestionType.toggleH1, + title: LocaleKeys.editor_toggleHeading1ShortForm.tr(), + svg: FlowySvgs.type_toggle_h1_m, + onTap: (state) => onToggleLevelChanged(state, 1), +); + +final toggleH2SuggestionItem = SuggestionItem( + type: SuggestionType.toggleH2, + title: LocaleKeys.editor_toggleHeading2ShortForm.tr(), + svg: FlowySvgs.type_toggle_h2_m, + onTap: (state) => onToggleLevelChanged(state, 2), +); + +final toggleH3SuggestionItem = SuggestionItem( + type: SuggestionType.toggleH3, + title: LocaleKeys.editor_toggleHeading3ShortForm.tr(), + svg: FlowySvgs.type_toggle_h3_m, + onTap: (state) => onToggleLevelChanged(state, 3), +); + +final callOutSuggestionItem = SuggestionItem( + type: SuggestionType.callOut, + title: LocaleKeys.document_plugins_callout.tr(), + svg: FlowySvgs.type_callout_m, + onTap: (state) { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + final isHighlight = node.type == CalloutBlockKeys.type; + state.formatNode( + selection, + (node) => node.copyWith( + type: isHighlight ? ParagraphBlockKeys.type : CalloutBlockKeys.type, + ), + ); + }, +); + +final quoteSuggestionItem = SuggestionItem( + type: SuggestionType.quote, + title: LocaleKeys.editor_quote.tr(), + svg: FlowySvgs.type_quote_m, + onTap: (state) { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + final isHighlight = node.type == QuoteBlockKeys.type; + state.formatNode( + selection, + (node) => node.copyWith( + type: isHighlight ? ParagraphBlockKeys.type : QuoteBlockKeys.type, + ), + ); + }, +); + +final suggestions = UnmodifiableListView([ + textSuggestionItem, + h1SuggestionItem, + h2SuggestionItem, + h3SuggestionItem, + checkboxSuggestionItem, + bulletedSuggestionItem, + numberedSuggestionItem, + toggleSuggestionItem, + toggleH1SuggestionItem, + toggleH2SuggestionItem, + toggleH3SuggestionItem, + callOutSuggestionItem, + quoteSuggestionItem, +]); + +final nodeType2SuggestionType = UnmodifiableMapView({ + ParagraphBlockKeys.type: SuggestionType.text, + NumberedListBlockKeys.type: SuggestionType.numbered, + BulletedListBlockKeys.type: SuggestionType.bulleted, + QuoteBlockKeys.type: SuggestionType.quote, + TodoListBlockKeys.type: SuggestionType.checkbox, + CalloutBlockKeys.type: SuggestionType.callOut, +}); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 10a5e117af..674b44e708 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -28,6 +28,8 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; + class EditorStyleCustomizer { EditorStyleCustomizer({ required this.context, @@ -59,6 +61,12 @@ class EditorStyleCustomizer { static double get optionMenuWidth => UniversalPlatform.isMobile ? 0 : 44; + static Color? toolbarHoverColor(BuildContext context) { + return Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).colorScheme.secondary + : AFThemeExtension.of(context).toolbarHoverColor; + } + EditorStyle style() { if (UniversalPlatform.isDesktopOrWeb) { return desktop(); @@ -305,10 +313,6 @@ class EditorStyleCustomizer { ); } - FloatingToolbarStyle floatingToolbarStyleBuilder() => FloatingToolbarStyle( - backgroundColor: Theme.of(context).colorScheme.onTertiary, - ); - TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { if (fontFamily == null || fontFamily == defaultFontFamily) { return TextStyle(fontWeight: fontWeight); @@ -484,7 +488,7 @@ class EditorStyleCustomizer { child = FlowyTooltip( richMessage: tooltipMessage, preferBelow: false, - verticalOffset: 20, + verticalOffset: 24, child: child, ); @@ -496,7 +500,7 @@ class EditorStyleCustomizer { if (!toolbarItemsWithoutHover.contains(id)) { child = Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 6), child: FlowyHover( style: HoverStyle( hoverColor: Colors.grey.withValues(alpha: 0.3), @@ -547,9 +551,9 @@ class EditorStyleCustomizer { ), TextSpan( text: (Platform.isMacOS ? '⌘+' : 'Ctrl+\\') + tooltip.$2, - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), + style: context.tooltipTextStyle()?.copyWith( + color: Theme.of(context).hintColor, + ), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index f788d99eb7..fc2a4507bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -70,7 +70,7 @@ class ExportTab extends StatelessWidget { const VSpace(10), _ExportButton( title: LocaleKeys.shareAction_csv.tr(), - svg: FlowySvgs.database_layout_m, + svg: FlowySvgs.database_layout_s, onTap: () => _exportCSV(context), ), if (kDebugMode) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index e1ea22a6eb..0b5c0c5f98 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -150,6 +150,7 @@ class DesktopAppearance extends BaseAppearance { scrollbarColor: theme.scrollbarColor, scrollbarHoverColor: theme.scrollbarHoverColor, lightIconColor: theme.lightIconColor, + toolbarHoverColor: theme.toolbarHoverColor, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 6aa649d320..eda3153459 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -280,6 +280,7 @@ class MobileAppearance extends BaseAppearance { scrollbarColor: theme.scrollbarColor, scrollbarHoverColor: theme.scrollbarHoverColor, lightIconColor: theme.lightIconColor, + toolbarHoverColor: theme.toolbarHoverColor, ), ToolbarColorExtension.fromBrightness(brightness), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart index 5d1c858d29..6c8eeb9ae4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart @@ -12,6 +12,8 @@ class SettingValueDropDown extends StatefulWidget { this.child, this.popoverController, this.offset, + this.boxConstraints, + this.margin = const EdgeInsets.all(6), }); final String currentValue; @@ -21,6 +23,8 @@ class SettingValueDropDown extends StatefulWidget { final Widget? child; final PopoverController? popoverController; final Offset? offset; + final BoxConstraints? boxConstraints; + final EdgeInsets margin; @override State createState() => _SettingValueDropDownState(); @@ -33,12 +37,14 @@ class _SettingValueDropDownState extends State { key: widget.popoverKey, controller: widget.popoverController, direction: PopoverDirection.bottomWithCenterAligned, + margin: widget.margin, popupBuilder: widget.popupBuilder, - constraints: const BoxConstraints( - minWidth: 80, - maxWidth: 160, - maxHeight: 400, - ), + constraints: widget.boxConstraints ?? + const BoxConstraints( + minWidth: 80, + maxWidth: 160, + maxHeight: 400, + ), offset: widget.offset, onClose: widget.onClose, child: widget.child ?? diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 9cd3a06313..4178edd294 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -90,6 +90,7 @@ class FlowyColorScheme { required this.scrollbarColor, required this.scrollbarHoverColor, required this.lightIconColor, + required this.toolbarHoverColor, }); final Color surface; @@ -154,6 +155,7 @@ class FlowyColorScheme { final Color scrollbarHoverColor; final Color lightIconColor; + final Color toolbarHoverColor; factory FlowyColorScheme.fromJson(Map json) => _$FlowyColorSchemeFromJson(json); diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index 2aa455b404..8d49b8dfa1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -86,6 +86,7 @@ class DandelionColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: const Color(0xFFF2F4F7), ); const DandelionColorScheme.dark() @@ -144,5 +145,6 @@ class DandelionColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index f829d3a67e..0e39de8fa8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -83,6 +83,7 @@ class DefaultColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: const Color(0xFFF2F4F7), ); const DefaultColorScheme.dark() @@ -141,5 +142,6 @@ class DefaultColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: ColorSchemeConstants.lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 97ff5221de..590d26db3e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -82,6 +82,7 @@ class LavenderColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: const Color(0xFFF2F4F7), ); const LavenderColorScheme.dark() @@ -140,5 +141,6 @@ class LavenderColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 5d9ff8b97c..3f39ae4c84 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -88,63 +88,64 @@ class LemonadeColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: const Color(0xFFF2F4F7), ); const LemonadeColorScheme.dark() : super( - surface: const Color(0xff292929), - hover: const Color(0xff1f1f1f), - selector: _darkShader2, - red: const Color(0xfffb006d), - yellow: const Color(0xffffd667), - green: const Color(0xff66cf80), - shader1: _white, - shader2: _darkShader2, - shader3: const Color(0xff828282), - shader4: const Color(0xffbdbdbd), - shader5: _darkShader5, - shader6: _darkShader6, - shader7: _white, - bg1: const Color(0xFFD5A200), - bg2: _black, - bg3: _darkMain1, - bg4: const Color(0xff2c144b), - tint1: const Color(0x4d9327FF), - tint2: const Color(0x66FC0088), - tint3: const Color(0x4dFC00E2), - tint4: const Color(0x80BE5B00), - tint5: const Color(0x33F8EE00), - tint6: const Color(0x4d6DC300), - tint7: const Color(0x5900BD2A), - tint8: const Color(0x80008890), - tint9: const Color(0x4d0029FF), - main1: _darkMain1, - main2: _darkMain1, - shadow: _black, - sidebarBg: const Color(0xff232B38), - divider: _darkShader3, - topbarBg: _darkShader1, - icon: _darkShader5, - text: _darkShader5, - secondaryText: _darkShader5, - strongText: Colors.white, - input: _darkInput, - hint: _darkShader5, - primary: _darkMain1, - onPrimary: _darkShader1, - hoverBG1: _darkMain1, - hoverBG2: _darkMain1, - hoverBG3: _darkShader3, - hoverFG: _darkShader1, - questionBubbleBG: _darkShader3, - progressBarBGColor: _darkShader3, - toolbarColor: _darkInput, - toggleButtonBGColor: _darkShader1, - calendarWeekendBGColor: const Color(0xff121212), - gridRowCountColor: _darkMain1, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - ); + surface: const Color(0xff292929), + hover: const Color(0xff1f1f1f), + selector: _darkShader2, + red: const Color(0xfffb006d), + yellow: const Color(0xffffd667), + green: const Color(0xff66cf80), + shader1: _white, + shader2: _darkShader2, + shader3: const Color(0xff828282), + shader4: const Color(0xffbdbdbd), + shader5: _darkShader5, + shader6: _darkShader6, + shader7: _white, + bg1: const Color(0xFFD5A200), + bg2: _black, + bg3: _darkMain1, + bg4: const Color(0xff2c144b), + tint1: const Color(0x4d9327FF), + tint2: const Color(0x66FC0088), + tint3: const Color(0x4dFC00E2), + tint4: const Color(0x80BE5B00), + tint5: const Color(0x33F8EE00), + tint6: const Color(0x4d6DC300), + tint7: const Color(0x5900BD2A), + tint8: const Color(0x80008890), + tint9: const Color(0x4d0029FF), + main1: _darkMain1, + main2: _darkMain1, + shadow: _black, + sidebarBg: const Color(0xff232B38), + divider: _darkShader3, + topbarBg: _darkShader1, + icon: _darkShader5, + text: _darkShader5, + secondaryText: _darkShader5, + strongText: Colors.white, + input: _darkInput, + hint: _darkShader5, + primary: _darkMain1, + onPrimary: _darkShader1, + hoverBG1: _darkMain1, + hoverBG2: _darkMain1, + hoverBG3: _darkShader3, + hoverFG: _darkShader1, + questionBubbleBG: _darkShader3, + progressBarBGColor: _darkShader3, + toolbarColor: _darkInput, + toggleButtonBGColor: _darkShader1, + calendarWeekendBGColor: const Color(0xff121212), + gridRowCountColor: _darkMain1, + borderColor: ColorSchemeConstants.darkBorderColor, + scrollbarColor: const Color(0x40FFFFFF), + scrollbarHoverColor: const Color(0x80FFFFFF), + lightIconColor: const Color(0xFF8F959E), + toolbarHoverColor: _lightShader6); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index a2bfd16b06..9ce1f0323d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -41,6 +41,7 @@ class AFThemeExtension extends ThemeExtension { required this.borderColor, required this.scrollbarColor, required this.scrollbarHoverColor, + required this.toolbarHoverColor, required this.lightIconColor, }); @@ -86,6 +87,7 @@ class AFThemeExtension extends ThemeExtension { final Color scrollbarColor; final Color scrollbarHoverColor; + final Color toolbarHoverColor; final Color lightIconColor; @override @@ -123,6 +125,7 @@ class AFThemeExtension extends ThemeExtension { Color? scrollbarColor, Color? scrollbarHoverColor, Color? lightIconColor, + Color? toolbarHoverColor, }) => AFThemeExtension( warning: warning ?? this.warning, @@ -159,6 +162,7 @@ class AFThemeExtension extends ThemeExtension { scrollbarColor: scrollbarColor ?? this.scrollbarColor, scrollbarHoverColor: scrollbarHoverColor ?? this.scrollbarHoverColor, lightIconColor: lightIconColor ?? this.lightIconColor, + toolbarHoverColor: toolbarHoverColor ?? this.toolbarHoverColor, ); @override @@ -215,6 +219,8 @@ class AFThemeExtension extends ThemeExtension { scrollbarHoverColor: Color.lerp(scrollbarHoverColor, other.scrollbarHoverColor, t)!, lightIconColor: Color.lerp(lightIconColor, other.lightIconColor, t)!, + toolbarHoverColor: + Color.lerp(toolbarHoverColor, other.toolbarHoverColor, t)!, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 190d840a41..9d25672622 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -144,7 +144,7 @@ class _PopoverContainer extends StatelessWidget { } } -extension on BuildContext { +extension PopoverDecoration on BuildContext { /// The decoration of the popover. /// /// Don't customize the entire decoration of the popover, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart new file mode 100644 index 0000000000..96a22a6f85 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart @@ -0,0 +1,43 @@ +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +class FlowyToolbarButton extends StatelessWidget { + final Widget child; + final VoidCallback? onPressed; + final EdgeInsets padding; + final String? tooltip; + + const FlowyToolbarButton({ + super.key, + this.onPressed, + this.tooltip, + this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 6), + required this.child, + }); + + @override + Widget build(BuildContext context) { + final tooltipMessage = tooltip ?? ''; + + return FlowyTooltip( + message: tooltipMessage, + padding: EdgeInsets.zero, + child: RawMaterialButton( + clipBehavior: Clip.antiAlias, + constraints: const BoxConstraints(minWidth: 36, minHeight: 32), + hoverElevation: 0, + highlightElevation: 0, + padding: EdgeInsets.zero, + shape: const RoundedRectangleBorder(borderRadius: Corners.s6Border), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + elevation: 0, + onPressed: onPressed, + child: child, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index a34a55b9f8..c770452fcd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -10,6 +10,7 @@ class FlowyTooltip extends StatelessWidget { this.preferBelow, this.margin, this.verticalOffset, + this.padding, this.child, }); @@ -19,6 +20,7 @@ class FlowyTooltip extends StatelessWidget { final EdgeInsetsGeometry? margin; final Widget? child; final double? verticalOffset; + final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -29,10 +31,11 @@ class FlowyTooltip extends StatelessWidget { return Tooltip( margin: margin, verticalOffset: verticalOffset ?? 16.0, - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), + padding: padding ?? + const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), decoration: BoxDecoration( color: context.tooltipBackgroundColor(), borderRadius: BorderRadius.circular(10.0), @@ -49,8 +52,10 @@ class FlowyTooltip extends StatelessWidget { extension FlowyToolTipExtension on BuildContext { double tooltipFontSize() => 14.0; + double tooltipHeight({double? fontSize}) => 20.0 / (fontSize ?? tooltipFontSize()); + Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light ? Colors.white : Colors.black; diff --git a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart index cbf114156d..e87bb3fa01 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart @@ -242,7 +242,13 @@ String varNameFor(File file, Options options) { return simplified; } -const sizeMap = {r'$16x': 's', r'$24x': 'm', r'$32x': 'lg', r'$40x': 'xl'}; +const sizeMap = { + r'$16x': 's', + r'$20x': 'm', + r'$24x': 'm', + r'$32x': 'lg', + r'$40x': 'xl' +}; /// cleans the path segment before rejoining the path into a variable name String clean(String segment) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 7537db010a..0cf46a664e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8db5bdf" - resolved-ref: "8db5bdff8f2f6258800660123452ddbde1c14059" + ref: "2b8c3af" + resolved-ref: "2b8c3af54802f71cfb9156ea2e824bc57faa3ec7" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index c537953906..4d362fd58b 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "8db5bdf" + ref: "2b8c3af" appflowy_editor_plugins: git: @@ -280,6 +280,7 @@ flutter: - assets/images/built_in_cover_images/ - assets/flowy_icons/ - assets/flowy_icons/16x/ + - assets/flowy_icons/20x/ - assets/flowy_icons/24x/ - assets/flowy_icons/32x/ - assets/flowy_icons/40x/ diff --git a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart b/frontend/appflowy_flutter/test/widget_test/test_material_app.dart index ecb06b97e2..2ebc61a8bc 100644 --- a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart +++ b/frontend/appflowy_flutter/test/widget_test/test_material_app.dart @@ -62,6 +62,7 @@ class WidgetTestApp extends StatelessWidget { scrollbarColor: Colors.transparent, scrollbarHoverColor: Colors.transparent, lightIconColor: Colors.transparent, + toolbarHoverColor: Colors.transparent, ), ], ), diff --git a/frontend/resources/flowy_icons/24x/calendar_layout.svg b/frontend/resources/flowy_icons/16x/calendar_layout.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/calendar_layout.svg rename to frontend/resources/flowy_icons/16x/calendar_layout.svg diff --git a/frontend/resources/flowy_icons/24x/close_filled.svg b/frontend/resources/flowy_icons/16x/close_filled.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/close_filled.svg rename to frontend/resources/flowy_icons/16x/close_filled.svg diff --git a/frontend/resources/flowy_icons/24x/database_layout.svg b/frontend/resources/flowy_icons/16x/database_layout.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/database_layout.svg rename to frontend/resources/flowy_icons/16x/database_layout.svg diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg new file mode 100644 index 0000000000..dd0390d2d5 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg new file mode 100644 index 0000000000..a8c8657135 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_alignment.svg b/frontend/resources/flowy_icons/20x/toolbar_alignment.svg new file mode 100644 index 0000000000..638ff3ece8 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_alignment.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg new file mode 100644 index 0000000000..6253ec74d4 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg new file mode 100644 index 0000000000..2e39539ab0 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_bold.svg b/frontend/resources/flowy_icons/20x/toolbar_bold.svg new file mode 100644 index 0000000000..a131c6aa3e --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_check.svg b/frontend/resources/flowy_icons/20x/toolbar_check.svg new file mode 100644 index 0000000000..e59186292c --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg new file mode 100644 index 0000000000..c263b0c66b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg new file mode 100644 index 0000000000..bc17a7b05b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_link.svg b/frontend/resources/flowy_icons/20x/toolbar_link.svg new file mode 100644 index 0000000000..8564d243d0 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_link.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_more.svg b/frontend/resources/flowy_icons/20x/toolbar_more.svg new file mode 100644 index 0000000000..d156f313a1 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_more.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg new file mode 100644 index 0000000000..87c67115fb --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg new file mode 100644 index 0000000000..bcaebfe5d0 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg new file mode 100644 index 0000000000..68069290ce --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg new file mode 100644 index 0000000000..f4b396705f --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_format.svg b/frontend/resources/flowy_icons/20x/toolbar_text_format.svg new file mode 100644 index 0000000000..0f3cd07a01 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_format.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg new file mode 100644 index 0000000000..49add4ba87 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_underline.svg b/frontend/resources/flowy_icons/20x/toolbar_underline.svg new file mode 100644 index 0000000000..ea467a45d6 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_underline.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_bulleted_list.svg b/frontend/resources/flowy_icons/20x/type_bulleted_list.svg new file mode 100644 index 0000000000..bc726f59ec --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_bulleted_list.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/20x/type_callout.svg b/frontend/resources/flowy_icons/20x/type_callout.svg new file mode 100644 index 0000000000..a933b4bbb3 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_callout.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_font.svg b/frontend/resources/flowy_icons/20x/type_font.svg new file mode 100644 index 0000000000..d0b33b0277 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_font.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_formula.svg b/frontend/resources/flowy_icons/20x/type_formula.svg new file mode 100644 index 0000000000..316c225b79 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_formula.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_h1.svg b/frontend/resources/flowy_icons/20x/type_h1.svg new file mode 100644 index 0000000000..a6a7f561cf --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_h1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_h2.svg b/frontend/resources/flowy_icons/20x/type_h2.svg new file mode 100644 index 0000000000..9bba1b7d33 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_h2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_h3.svg b/frontend/resources/flowy_icons/20x/type_h3.svg new file mode 100644 index 0000000000..3b231df67d --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_h3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_numbered_list.svg b/frontend/resources/flowy_icons/20x/type_numbered_list.svg new file mode 100644 index 0000000000..23046f9b34 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_numbered_list.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/type_page.svg b/frontend/resources/flowy_icons/20x/type_page.svg new file mode 100644 index 0000000000..405548fcf7 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_page.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_quote.svg b/frontend/resources/flowy_icons/20x/type_quote.svg new file mode 100644 index 0000000000..3564d92ff8 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_strikethrough.svg b/frontend/resources/flowy_icons/20x/type_strikethrough.svg new file mode 100644 index 0000000000..dbf4e86116 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_strikethrough.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_text.svg b/frontend/resources/flowy_icons/20x/type_text.svg new file mode 100644 index 0000000000..40335aa89b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_text.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_todo.svg b/frontend/resources/flowy_icons/20x/type_todo.svg new file mode 100644 index 0000000000..3d4f38ae9f --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_todo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h1.svg b/frontend/resources/flowy_icons/20x/type_toggle_h1.svg new file mode 100644 index 0000000000..45cc7d3859 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_toggle_h1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h2.svg b/frontend/resources/flowy_icons/20x/type_toggle_h2.svg new file mode 100644 index 0000000000..3dce8523e8 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_toggle_h2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h3.svg b/frontend/resources/flowy_icons/20x/type_toggle_h3.svg new file mode 100644 index 0000000000..e619a5f250 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_toggle_h3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/type_toggle_list.svg b/frontend/resources/flowy_icons/20x/type_toggle_list.svg new file mode 100644 index 0000000000..2cb1e83599 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/type_toggle_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3435fedbb6..9eb6664cf3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -331,8 +331,7 @@ "header": "Header", "highlight": "Highlight", "color": "Color", - "addLink": "Add Link", - "link": "Link" + "addLink": "Add Link" }, "tooltip": { "lightMode": "Switch to Light mode", @@ -2101,7 +2100,20 @@ "morePages": "more pages" }, "toolbar": { - "resetToDefaultFont": "Reset to default" + "resetToDefaultFont": "Reset to default", + "textSize": "Text Size", + "h1": "Heading 1", + "h2": "Heading 2", + "h3": "Heading 3", + "alignLeft": "Align Left", + "alignRight": "Align Right", + "alignCenter": "Align Center", + "link": "Link", + "textAlign": "Text Align", + "moreOptions": "More Options", + "font": "Font", + "suggestions": "Suggestions", + "turnInto": "Turn Into" }, "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", From c7d3d612ae87737600284e56b4acf51d40fddbbd Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 13 Mar 2025 13:57:58 +0800 Subject: [PATCH 130/384] fix: emoji picker position error (#7497) * fix: click an emoji should close the menu when using /emoji to insert an emoji into a doc * fix: emoji picker position error * fix: document emoji icon is clipped on Android --- .../header/emoji_icon_widget.dart | 11 +++---- .../widgets/emoji_picker/emoji_menu_item.dart | 31 +++++++++++++++++-- .../emoji_picker/emoji_shortcut_event.dart | 8 ++--- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 99b50c49c9..33333a3e92 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -111,13 +111,10 @@ class _RawEmojiIconWidgetState extends State { try { switch (widget.emoji.type) { case FlowyIconType.emoji: - return SizedBox( - width: widget.emojiSize, - child: EmojiText( - emoji: widget.emoji.emoji, - fontSize: widget.emojiSize, - textAlign: TextAlign.justify, - ), + return EmojiText( + emoji: widget.emoji.emoji, + fontSize: widget.emojiSize, + textAlign: TextAlign.justify, ); case FlowyIconType.icon: IconsData iconData = diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index ab952386bd..72aed27ad4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -33,15 +33,39 @@ void showEmojiPickerMenu( Alignment alignment, Offset offset, ) { - final top = alignment == Alignment.topLeft ? offset.dy : null; - final bottom = alignment == Alignment.bottomLeft ? offset.dy : null; + (double? left, double? top, double? right, double? bottom) getPosition() { + double? left, top, right, bottom; + switch (alignment) { + case Alignment.topLeft: + left = offset.dx; + top = offset.dy; + break; + case Alignment.bottomLeft: + left = offset.dx; + bottom = offset.dy; + break; + case Alignment.topRight: + right = offset.dx; + top = offset.dy; + break; + case Alignment.bottomRight: + right = offset.dx; + bottom = offset.dy; + break; + } + + return (left, top, right, bottom); + } + + final (left, top, right, bottom) = getPosition(); keepEditorFocusNotifier.increase(); late OverlayEntry emojiPickerMenuEntry; emojiPickerMenuEntry = FullScreenOverlayEntry( + left: left, top: top, bottom: bottom, - left: offset.dx, + right: right, dismissCallback: () => keepEditorFocusNotifier.decrease(), builder: (context) => Material( type: MaterialType.transparency, @@ -56,6 +80,7 @@ void showEmojiPickerMenu( child: EmojiSelectionMenu( onSubmitted: (emoji) { editorState.insertTextAtCurrentSelection(emoji); + emojiPickerMenuEntry.remove(); }, onExit: () { // close emoji panel diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index 078cf64963..5bb4766353 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -35,7 +35,7 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { // Calculate the offset and alignment // Don't like these values being hardcoded but unsure how to grab the // values dynamically to match the /emoji command. - const menuHeight = 200.0; + const menuHeight = 380.0; const menuOffset = Offset(10, 10); // Tried (0, 10) but that looked off final editorOffset = @@ -47,7 +47,7 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { alignment = Alignment.topLeft; final bottomRight = rect.bottomRight; final topRight = rect.topRight; - final newOffset = bottomRight + menuOffset; + var newOffset = bottomRight + menuOffset; offset = Offset( newOffset.dx, newOffset.dy, @@ -55,12 +55,12 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { // show above if (newOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { - offset = topRight - menuOffset; + newOffset = topRight - menuOffset; alignment = Alignment.bottomLeft; offset = Offset( newOffset.dx, - MediaQuery.of(context).size.height - newOffset.dy, + editorHeight + editorOffset.dy - newOffset.dy, ); } From 9bd13ac29edbe4a419b1f08fea93f8f407655b9c Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 13 Mar 2025 13:58:06 +0800 Subject: [PATCH 131/384] fix: the slash menu position sometimes is wrong (#7492) --- .../selection_menu/mobile_selection_menu.dart | 158 ++++++++++++------ 1 file changed, 109 insertions(+), 49 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart index 07eceac51a..7f293cc1c9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart @@ -36,12 +36,16 @@ class MobileSelectionMenu extends SelectionMenuService { Alignment _alignment = Alignment.topLeft; final int itemCountFilter; final int startOffset; + ValueNotifier<_Position> _positionNotifier = ValueNotifier(_Position.zero); @override void dismiss() { if (_selectionMenuEntry != null) { editorState.service.keyboardService?.enable(); editorState.service.scrollService?.enable(); + editorState + .removeScrollViewScrolledListener(_checkPositionAfterScrolling); + _positionNotifier.dispose(); } _selectionMenuEntry?.remove(); @@ -53,23 +57,20 @@ class MobileSelectionMenu extends SelectionMenuService { final completer = Completer(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _show(); + editorState.addScrollViewScrolledListener(_checkPositionAfterScrolling); completer.complete(); }); return completer.future; } void _show() { - final selectionRects = editorState.selectionRects(); - if (selectionRects.isEmpty) { - return; - } - - calculateSelectionMenuOffset(selectionRects.first); - final (left, top, right, bottom) = getPosition(); + final position = _getCurrentPosition(); + if (position == null) return; final editorHeight = editorState.renderBox!.size.height; final editorWidth = editorState.renderBox!.size.width; + _positionNotifier = ValueNotifier(position); _selectionMenuEntry = OverlayEntry( builder: (context) { return SizedBox( @@ -80,47 +81,54 @@ class MobileSelectionMenu extends SelectionMenuService { onTap: dismiss, child: Stack( children: [ - Positioned( - top: top, - bottom: bottom, - left: left, - right: right, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: MobileSelectionMenuWidget( - selectionMenuStyle: style, - singleColumn: singleColumn, - items: selectionMenuItems - ..forEach((element) { - if (element is MobileSelectionMenuItem) { - element.deleteSlash = false; - element.deleteKeywords = deleteKeywordsByDefault; - for (final e in element.children) { - e.deleteSlash = deleteSlashByDefault; - e.deleteKeywords = deleteKeywordsByDefault; - e.onSelected = () { - dismiss(); - }; - } - } else { - element.deleteSlash = deleteSlashByDefault; - element.deleteKeywords = deleteKeywordsByDefault; - element.onSelected = () { - dismiss(); - }; - } - }), - maxItemInRow: 5, - editorState: editorState, - itemCountFilter: itemCountFilter, - startOffset: startOffset, - menuService: this, - onExit: () { - dismiss(); - }, - deleteSlashByDefault: deleteSlashByDefault, - ), - ), + ValueListenableBuilder( + valueListenable: _positionNotifier, + builder: (context, value, _) { + return Positioned( + top: value.top, + bottom: value.bottom, + left: value.left, + right: value.right, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MobileSelectionMenuWidget( + selectionMenuStyle: style, + singleColumn: singleColumn, + items: selectionMenuItems + ..forEach((element) { + if (element is MobileSelectionMenuItem) { + element.deleteSlash = false; + element.deleteKeywords = + deleteKeywordsByDefault; + for (final e in element.children) { + e.deleteSlash = deleteSlashByDefault; + e.deleteKeywords = deleteKeywordsByDefault; + e.onSelected = () { + dismiss(); + }; + } + } else { + element.deleteSlash = deleteSlashByDefault; + element.deleteKeywords = + deleteKeywordsByDefault; + element.onSelected = () { + dismiss(); + }; + } + }), + maxItemInRow: 5, + editorState: editorState, + itemCountFilter: itemCountFilter, + startOffset: startOffset, + menuService: this, + onExit: () { + dismiss(); + }, + deleteSlashByDefault: deleteSlashByDefault, + ), + ), + ); + }, ), ], ), @@ -135,6 +143,34 @@ class MobileSelectionMenu extends SelectionMenuService { editorState.service.scrollService?.disable(); } + /// the workaround for: editor auto scrolling that will cause wrong position + /// of slash menu + void _checkPositionAfterScrolling() { + final position = _getCurrentPosition(); + if (position == null) return; + if (position == _positionNotifier.value) { + Future.delayed(const Duration(milliseconds: 100)).then((_) { + final position = _getCurrentPosition(); + if (position == null) return; + if (position != _positionNotifier.value) { + _positionNotifier.value = position; + } + }); + } else { + _positionNotifier.value = position; + } + } + + _Position? _getCurrentPosition() { + final selectionRects = editorState.selectionRects(); + if (selectionRects.isEmpty) { + return null; + } + calculateSelectionMenuOffset(selectionRects.first); + final (left, top, right, bottom) = getPosition(); + return _Position(left, top, right, bottom); + } + @override Alignment get alignment { return _alignment; @@ -166,7 +202,6 @@ class MobileSelectionMenu extends SelectionMenuService { bottom = offset.dy; break; } - return (left, top, right, bottom); } @@ -217,3 +252,28 @@ class MobileSelectionMenu extends SelectionMenuService { } } } + +class _Position { + const _Position(this.left, this.top, this.right, this.bottom); + + final double? left; + final double? top; + final double? right; + final double? bottom; + + static const _Position zero = _Position(0, 0, 0, 0); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _Position && + runtimeType == other.runtimeType && + left == other.left && + top == other.top && + right == other.right && + bottom == other.bottom; + + @override + int get hashCode => + left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode; +} From 651046ab68f20486360f051bdf925e19fce8e2c7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 13 Mar 2025 13:58:24 +0800 Subject: [PATCH 132/384] fix: unable to paste iframe in editor (#7525) --- .../editor_plugins/copy_and_paste/paste_from_plain_text.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart index 90ed451128..4728eb7cf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart @@ -32,8 +32,11 @@ extension PasteFromPlainText on EditorState { await deleteSelectionIfNeeded(); + /// try to parse the plain text as markdown final nodes = customMarkdownToDocument(plainText).root.children; if (nodes.isEmpty) { + /// if the markdown parser failed, fallback to the plain text parser + await pastePlainText(plainText); return; } if (nodes.length == 1) { From 81bac5950c185914833ce22e114c5d73ba2f2c94 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 14:01:41 +0800 Subject: [PATCH 133/384] chore: try to fix windows flashing window issue --- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/Cargo.toml | 2 +- .../flowy-ai/src/local_ai/resource.rs | 61 +++++++++++-------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index c7d563e256..4e1619044c 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0#cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f4000af60ac18ad36a5dfb3fdc72c6d3ae967127#f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0#cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f4000af60ac18ad36a5dfb3fdc72c6d3ae967127#f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ccbfbca269..42120203cc 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cca2e80536dc2e0a994931f43b0f1c0fd9d4abd0" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 145ce41d3b..2db562aa0d 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -36,7 +36,7 @@ tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } appflowy-plugin = { version = "0.1.0" } -reqwest = "0.11.27" +reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" base64 = "0.21.5" futures-util = "0.3.30" diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 1380ed38ec..4a9403d670 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -14,11 +14,22 @@ use crate::notification::{ use appflowy_local_ai::ollama_plugin::OllamaPluginConfig; use lib_infra::util::{get_operating_system, OperatingSystem}; use reqwest::Client; +use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tracing::{error, info, instrument, trace}; +#[derive(Debug, Deserialize)] +struct TagsResponse { + models: Vec, +} + +#[derive(Debug, Deserialize)] +struct ModelEntry { + name: String, +} + #[async_trait] pub trait LLMResourceService: Send + Sync + 'static { /// Get local ai configuration from remote server @@ -155,7 +166,6 @@ impl LocalAIResourceController { resources.pop().map(|r| r.desc()) } - /// Returns true when all resources are downloaded and ready to use. pub async fn calculate_pending_resources(&self) -> FlowyResult> { let mut resources = vec![]; let app_path = ollama_plugin_path(); @@ -184,39 +194,40 @@ impl LocalAIResourceController { }, } - let required_models = vec![ - setting.chat_model_name, - setting.embedding_model_name, - // Add any additional required models here. - ]; - match tokio::process::Command::new("ollama") - .arg("list") - .output() - .await - { - Ok(output) if output.status.success() => { - let stdout = String::from_utf8_lossy(&output.stdout); - for model in &required_models { - if !stdout.contains(model.as_str()) { + let required_models = vec![setting.chat_model_name, setting.embedding_model_name]; + + // Query the /api/tags endpoint to get a structured list of locally available models. + let tags_url = format!("{}/api/tags", setting.ollama_server_url); + + match client.get(&tags_url).send().await { + Ok(resp) if resp.status().is_success() => { + let tags: TagsResponse = resp.json().await.map_err(|e| { + log::error!( + "[LLM Resource] Failed to parse /api/tags JSON response: {:?}", + e + ); + e + })?; + // Check each required model is present in the response. + for required in &required_models { + if !tags.models.iter().any(|m| m.name.contains(required)) { log::trace!( - "[LLM Resource] required model '{}' not found in ollama list", - model + "[LLM Resource] required model '{}' not found in API response", + required ); - resources.push(PendingResource::MissingModel(model.clone())); + resources.push(PendingResource::MissingModel(required.clone())); + // Optionally, you could continue checking all models rather than returning early. return Ok(resources); } } }, - Ok(output) => { + _ => { error!( - "[LLM Resource] 'ollama list' command failed with status: {:?}", - output.status + "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", + setting.ollama_server_url ); resources.push(PendingResource::OllamaServerNotReady); - }, - Err(e) => { - error!("[LLM Resource] failed to execute 'ollama list': {:?}", e); - resources.push(PendingResource::OllamaServerNotReady); + return Ok(resources); }, } From 133eec8163efcd36c6e93b29585b8b8eb858c1fe Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 13 Mar 2025 14:30:09 +0800 Subject: [PATCH 134/384] chore: update dsb_pub.pem on Windows (#7528) --- frontend/appflowy_flutter/dsa_pub.pem | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/appflowy_flutter/dsa_pub.pem diff --git a/frontend/appflowy_flutter/dsa_pub.pem b/frontend/appflowy_flutter/dsa_pub.pem new file mode 100644 index 0000000000..6a9d213b8a --- /dev/null +++ b/frontend/appflowy_flutter/dsa_pub.pem @@ -0,0 +1,36 @@ +-----BEGIN PUBLIC KEY----- +MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT +rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG +4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw ++sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV +KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5 +b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z +QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW +YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG +G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu +6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA +6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp +q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd +0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/ +4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb +K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7 +hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO +s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz +Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4 +uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV +Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn +ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB ++fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN +C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r +vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx +k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y +GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/ +eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG +hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM +EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8 +iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI +7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb +w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf +1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P +Y29SB4jvwqls268rP0cWqy4WXwlVwuc= +-----END PUBLIC KEY----- From d94b4daa70e4923806e39c7e07e224e6cecc6e6a Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 15:29:45 +0800 Subject: [PATCH 135/384] chore: fix flashing window --- frontend/rust-lib/flowy-ai/src/local_ai/watch.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index da6c30a69a..895166d154 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -121,9 +121,11 @@ pub(crate) fn ollama_plugin_command_available() -> bool { if cfg!(windows) { #[cfg(windows)] { - // 1. Try "where" command first + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; let output = Command::new("cmd") - .args(["/C", "where", "ollama_ai_plugin"]) + .args(&["/C", "where", "ollama_ai_plugin"]) + .creation_flags(CREATE_NO_WINDOW) .output(); if let Ok(output) = output { if !output.stdout.is_empty() { From 86b67a1b650e229170daa43505ee02066615ef74 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 15:49:44 +0800 Subject: [PATCH 136/384] chore: fix flashing window --- .../flowy-ai/src/local_ai/controller.rs | 23 ++++++++++++++----- .../flowy-ai/src/local_ai/resource.rs | 16 ++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 11a0e9a480..842db994b8 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use crate::local_ai::watch::is_plugin_ready; use crate::stream_message::StreamMessage; use appflowy_local_ai::ollama_plugin::OllamaAIPlugin; +use appflowy_plugin::core::plugin::RunningState; use arc_swap::ArcSwapOption; use futures_util::SinkExt; use lib_infra::util::get_operating_system; @@ -97,11 +98,16 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - let ready = is_plugin_ready(); - let lack_of_resource = cloned_llm_res.get_lack_of_resource().await; let new_state = RunningStatePB::from(state); let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + let mut ready = false; + let mut lack_of_resource = None; + if enabled { + ready = is_plugin_ready(); + lack_of_resource = cloned_llm_res.get_lack_of_resource().await; + } + chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::UpdateLocalAIState, @@ -259,9 +265,14 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let is_app_downloaded = is_plugin_ready(); - let state = self.ai_plugin.get_plugin_running_state(); - let lack_of_resource = self.resource.get_lack_of_resource().await; + let mut is_plugin_executable_ready = false; + let mut state = RunningState::ReadyToConnect; + let mut lack_of_resource = None; + if enabled { + is_plugin_executable_ready = is_plugin_ready(); + state = self.ai_plugin.get_plugin_running_state(); + lack_of_resource = self.resource.get_lack_of_resource().await; + } let elapsed = start.elapsed(); debug!( "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", @@ -270,7 +281,7 @@ impl LocalAIController { ); LocalAIPB { enabled, - is_plugin_executable_ready: is_app_downloaded, + is_plugin_executable_ready, state: RunningStatePB::from(state), lack_of_resource, } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 4a9403d670..0fc09bbc8d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -237,21 +237,15 @@ impl LocalAIResourceController { #[instrument(level = "info", skip_all)] pub async fn get_plugin_config(&self, rag_enabled: bool) -> FlowyResult { if !self.is_resource_ready().await { - return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); + return Err(FlowyError::new( + ErrorCode::AppFlowyLAINotReady, + "AppFlowyLAI not found", + )); } let llm_setting = self.get_llm_setting(); let bin_path = match get_operating_system() { - OperatingSystem::MacOS | OperatingSystem::Windows => { - if !is_plugin_ready() { - return Err(FlowyError::new( - ErrorCode::AppFlowyLAINotReady, - "AppFlowyLAI not found", - )); - } - - ollama_plugin_path() - }, + OperatingSystem::MacOS | OperatingSystem::Windows => ollama_plugin_path(), _ => { return Err( FlowyError::local_ai_unavailable() From 723971e4236ca88a4c10566bf9c0634296c217b8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 16:44:41 +0800 Subject: [PATCH 137/384] chore: fix editable --- .../lib/ai/service/ai_prompt_input_bloc.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 7e05bffca9..16ca6bd869 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -39,8 +39,8 @@ class AIPromptInputBloc extends Bloc { (event, emit) { event.when( updateAIState: (localAIState) { - AiType aiType = localAIState.enabled ? AiType.local : AiType.cloud; - bool supportChatWithFile = + final aiType = localAIState.enabled ? AiType.local : AiType.cloud; + final supportChatWithFile = aiType.isLocal && localAIState.state == RunningStatePB.Running; // If local ai is enabled, user can only send messages when the AI is running @@ -48,11 +48,6 @@ class AIPromptInputBloc extends Bloc { ? localAIState.state == RunningStatePB.Running : true; - if (localAIState.hasLackOfResource()) { - aiType = AiType.cloud; - supportChatWithFile = false; - } - var hintText = aiType.isLocal ? LocaleKeys.chat_inputLocalAIMessageHint.tr() : LocaleKeys.chat_inputMessageHint.tr(); From 0657aeb07d91c95900d3de49664124dc402c3d7b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 17:02:32 +0800 Subject: [PATCH 138/384] chore: set local ai default value --- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 842db994b8..eafaa86b85 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -198,7 +198,7 @@ impl LocalAIController { .workspace_id() .map(|workspace_id| local_ai_enabled_key(&workspace_id)) { - self.store_preferences.get_bool(&key).unwrap_or(true) + self.store_preferences.get_bool(&key).unwrap_or(false) } else { false } From 3ad8f624cf2f5d49f44242930be2a4db11f3706b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 17:03:49 +0800 Subject: [PATCH 139/384] chore: enable linux local ai --- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 0fc09bbc8d..a8ee83a256 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -245,7 +245,9 @@ impl LocalAIResourceController { let llm_setting = self.get_llm_setting(); let bin_path = match get_operating_system() { - OperatingSystem::MacOS | OperatingSystem::Windows => ollama_plugin_path(), + OperatingSystem::MacOS | OperatingSystem::Windows | OperatingSystem::Linux => { + ollama_plugin_path() + }, _ => { return Err( FlowyError::local_ai_unavailable() From 79967365924cbd4f3a64dc9c2f59ade6360d4421 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 17:27:55 +0800 Subject: [PATCH 140/384] chore: enable linux local ai --- .../pages/setting_ai_view/plugin_state.dart | 15 +++++++++++- frontend/resources/translations/en.json | 2 +- .../flowy-ai/src/local_ai/controller.rs | 23 +++++++++---------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index 9e979e9a5a..0976dc6921 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -25,7 +25,7 @@ class PluginStateIndicator extends StatelessWidget { builder: (context, state) { return state.action.when( unknown: () => const SizedBox.shrink(), - readToRun: () => const SizedBox.shrink(), + readToRun: () => const _PrepareRunning(), initializingPlugin: () => const InitLocalAIIndicator(), running: () => const _LocalAIRunning(), restartPlugin: () => const _RestartPluginButton(), @@ -37,6 +37,19 @@ class PluginStateIndicator extends StatelessWidget { } } +class _PrepareRunning extends StatelessWidget { + const _PrepareRunning(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()), + ], + ); + } +} + class _RestartPluginButton extends StatelessWidget { const _RestartPluginButton(); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9eb6664cf3..80798e1ff6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -846,7 +846,7 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI Chat is starting...", + "localAIStart": "Local AI is starting...", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index eafaa86b85..b4191d15b5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -147,7 +147,7 @@ impl LocalAIController { resource: &Arc, ai_plugin: &Arc, ) { - if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None, false).await { + if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None).await { error!("[AI Plugin] failed to setup plugin: {:?}", err); } } @@ -289,7 +289,7 @@ impl LocalAIController { #[instrument(level = "debug", skip_all)] pub async fn restart_plugin(&self) { - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None, true).await { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { error!("[AI Plugin] failed to setup plugin: {:?}", err); } } @@ -441,8 +441,7 @@ impl LocalAIController { ); if enabled { let (tx, rx) = tokio::sync::oneshot::channel(); - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx), false).await - { + if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx)).await { error!("[AI Plugin] failed to initialize local ai: {:?}", err); } let _ = rx.await; @@ -472,17 +471,10 @@ async fn initialize_ai_plugin( plugin: &Arc, llm_resource: &Arc, ret: Option>, - force: bool, ) -> FlowyResult<()> { let plugin = plugin.clone(); - - if !force { - if plugin.get_plugin_running_state().is_loading() { - return Ok(()); - } - } - let lack_of_resource = llm_resource.get_lack_of_resource().await; + chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::UpdateLocalAIState, @@ -510,6 +502,13 @@ async fn initialize_ai_plugin( }) .send(); + if let Err(err) = plugin.destroy_plugin().await { + error!( + "[AI Plugin] failed to destroy plugin when lack of resource: {:?}", + err + ); + } + return Ok(()); } From 22b9acf386afe9dd58fc906a1e10903a1c8af1a9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 13 Mar 2025 19:51:42 +0800 Subject: [PATCH 141/384] chore: send local ai state --- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index b4191d15b5..c0475aa88f 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -310,10 +310,7 @@ impl LocalAIController { let key = local_ai_enabled_key(&workspace_id); let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); self.store_preferences.set_bool(&key, enabled)?; - - if self.resource.is_resource_ready().await { - self.toggle_plugin(enabled).await?; - } + self.toggle_plugin(enabled).await?; Ok(enabled) } From 36bf90e81bb23cf2fcc55de925d922f977cc80d1 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:10:29 +0800 Subject: [PATCH 142/384] fix: stop generating in ai chat and writer (#7534) * fix: stop generating shortcuts not working edge cases * chore: add tooltip --- .../ai/widgets/prompt_input/send_button.dart | 19 ++++++++++++++ .../lib/plugins/ai_chat/chat_page.dart | 26 +++++++++++++------ frontend/resources/translations/en.json | 4 ++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart index 20def4ca5e..cca6e65f63 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart @@ -1,4 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +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:universal_platform/universal_platform.dart'; @@ -23,6 +25,23 @@ class PromptInputSendButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyIconButton( width: _buttonSize, + richTooltipText: switch (state) { + SendButtonState.streaming => TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.chat_stopTooltip.tr()} ', + style: context.tooltipTextStyle(), + ), + TextSpan( + text: 'ESC', + style: context + .tooltipTextStyle() + ?.copyWith(color: Theme.of(context).hintColor), + ), + ], + ), + _ => null, + }, icon: switch (state) { SendButtonState.enabled => FlowySvg( FlowySvgs.ai_send_filled_s, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 2d0c05b2c4..5400297870 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -93,14 +93,24 @@ class AIChatPage extends StatelessWidget { } } }, - child: CallbackShortcuts( - bindings: { - SingleActivator(LogicalKeyboardKey.escape): () { - context.read().add(ChatEvent.stopStream()); - }, - SingleActivator(control: true, LogicalKeyboardKey.keyC): () { - context.read().add(ChatEvent.stopStream()); - }, + child: FocusScope( + onKeyEvent: (focusNode, event) { + if (event is! KeyUpEvent) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.escape || + event.logicalKey == LogicalKeyboardKey.keyC && + HardwareKeyboard.instance.isControlPressed) { + final chatBloc = context.read(); + if (chatBloc.state.promptResponseState != + PromptResponseState.ready) { + chatBloc.add(ChatEvent.stopStream()); + return KeyEventResult.handled; + } + } + + return KeyEventResult.ignored; }, child: _ChatContentPage( view: view, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 80798e1ff6..42dbb89155 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -256,7 +256,9 @@ "selectMessages": "Select messages", "nSelected": "{} selected", "allSelected": "All selected" - } + }, + "stopTooltip": "Stop generating" + }, "trash": { "text": "Trash", From 6ac9ad1cac5fe7bf67343f514d185609c9655ddf Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:10:40 +0800 Subject: [PATCH 143/384] fix: continue writing edge case (#7535) --- .../lib/plugins/document/document_page.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 4e15128788..46ef2ca7f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -55,6 +55,8 @@ class _DocumentPageState extends State Selection? initialSelection; late final documentBloc = DocumentBloc(documentId: widget.view.id) ..add(const DocumentEvent.initial()); + late final viewBloc = ViewBloc(view: widget.view) + ..add(const ViewEvent.initial()); @override void initState() { @@ -66,6 +68,7 @@ class _DocumentPageState extends State void dispose() { WidgetsBinding.instance.removeObserver(this); documentBloc.close(); + viewBloc.close(); super.dispose(); } @@ -88,14 +91,9 @@ class _DocumentPageState extends State BlocProvider.value(value: documentBloc), BlocProvider.value( value: ViewLockStatusBloc(view: widget.view) - ..add( - ViewLockStatusEvent.initial(), - ), - ), - BlocProvider( - create: (_) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + ..add(ViewLockStatusEvent.initial()), ), + BlocProvider.value(value: viewBloc), ], child: BlocConsumer( listenWhen: (prev, curr) => curr.isLocked != prev.isLocked, From e36b08cd14518eb0915c2a17258693f52e35c119 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:10:52 +0800 Subject: [PATCH 144/384] chore: add tooltip for disabled select messages (#7536) --- .../lib/plugins/ai_chat/chat.dart | 3 ++ .../widgets/common_view_action.dart | 37 ++++++++++--------- frontend/resources/translations/en.json | 3 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index b840472854..76aba27dc0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -181,6 +181,9 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder disabled: !state.enabled, leftIcon: FlowySvgs.ai_add_to_page_s, label: LocaleKeys.moreAction_saveAsNewPage.tr(), + tooltipMessage: state.enabled + ? null + : LocaleKeys.moreAction_saveAsNewPageDisabled.tr(), onTap: () { chatMessageSelectorBloc.add( const ChatSelectMessageEvent diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart index 6fad559ba4..2ecec3244c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart @@ -10,10 +10,8 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -107,6 +105,7 @@ class CustomViewAction extends StatelessWidget { required this.view, required this.leftIcon, required this.label, + this.tooltipMessage, this.disabled = false, this.onTap, this.mutex, @@ -116,6 +115,7 @@ class CustomViewAction extends StatelessWidget { final FlowySvgData leftIcon; final String label; final bool disabled; + final String? tooltipMessage; final VoidCallback? onTap; final PopoverMutex? mutex; @@ -124,20 +124,23 @@ class CustomViewAction extends StatelessWidget { return Container( height: 34, padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - disable: disabled, - onTap: onTap, - leftIcon: FlowySvg( - leftIcon, - size: const Size.square(16.0), - color: disabled ? Theme.of(context).disabledColor : null, - ), - iconPadding: 10.0, - text: FlowyText( - label, - figmaLineHeight: 18.0, - color: disabled ? Theme.of(context).disabledColor : null, + child: FlowyTooltip( + message: tooltipMessage, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + disable: disabled, + onTap: onTap, + leftIcon: FlowySvg( + leftIcon, + size: const Size.square(16.0), + color: disabled ? Theme.of(context).disabledColor : null, + ), + iconPadding: 10.0, + text: FlowyText( + label, + figmaLineHeight: 18.0, + color: disabled ? Theme.of(context).disabledColor : null, + ), ), ), ); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 42dbb89155..3face24fa5 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -151,7 +151,8 @@ "charCountLabel": "Character count: ", "createdAtLabel": "Created: ", "syncedAtLabel": "Synced: ", - "saveAsNewPage": "Add messages to page" + "saveAsNewPage": "Add messages to page", + "saveAsNewPageDisabled": "No messages available" }, "importPanel": { "textAndMarkdown": "Text & Markdown", From 1fdd7c343bb0e596e819e89eb9d9965404a91498 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:31:05 +0800 Subject: [PATCH 145/384] chore: use tooltip instead of multi line for related questions (#7533) --- .../presentation/chat_related_question.dart | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index cc21cfc80e..34427479fe 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -66,25 +67,28 @@ class RelatedQuestionItem extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyButton( - mainAxisAlignment: MainAxisAlignment.start, - text: Flexible( - child: FlowyText( - question, - lineHeight: 1.4, - maxLines: null, + return FlowyTooltip( + message: question, + child: FlowyButton( + mainAxisAlignment: MainAxisAlignment.start, + text: Flexible( + child: FlowyText( + question, + lineHeight: 1.4, + overflow: TextOverflow.ellipsis, + ), ), + expandText: false, + margin: UniversalPlatform.isMobile + ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0) + : const EdgeInsets.all(8.0), + leftIcon: FlowySvg( + FlowySvgs.ai_chat_outlined_s, + color: Theme.of(context).colorScheme.primary, + size: const Size.square(16.0), + ), + onTap: () => onQuestionSelected(question), ), - expandText: false, - margin: UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0) - : const EdgeInsets.all(8.0), - leftIcon: FlowySvg( - FlowySvgs.ai_chat_outlined_s, - color: Theme.of(context).colorScheme.primary, - size: const Size.square(16.0), - ), - onTap: () => onQuestionSelected(question), ); } } From e10aade895f1f59761b8f59f7a9cd0c9e1966998 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 11:36:21 +0800 Subject: [PATCH 146/384] chore: update local ai client --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4e1619044c..3ed4794177 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f4000af60ac18ad36a5dfb3fdc72c6d3ae967127#f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b1117aa0826be0fed1c543019a9f3187fb99b0e7#b1117aa0826be0fed1c543019a9f3187fb99b0e7" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f4000af60ac18ad36a5dfb3fdc72c6d3ae967127#f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b1117aa0826be0fed1c543019a9f3187fb99b0e7#b1117aa0826be0fed1c543019a9f3187fb99b0e7" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 42120203cc..9c17eb1d7c 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f4000af60ac18ad36a5dfb3fdc72c6d3ae967127" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b1117aa0826be0fed1c543019a9f3187fb99b0e7" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b1117aa0826be0fed1c543019a9f3187fb99b0e7" } From 58895620c193f739fa674b6835a2328cf953043d Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 13:23:41 +0800 Subject: [PATCH 147/384] chore: update local ai logs --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +++++++++---------- .../desktop_prompt_text_field.dart | 15 +++++- frontend/resources/translations/en.json | 5 +- .../flowy-ai/src/local_ai/controller.rs | 10 +++- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 4b7ed5d639..92e52a1a79 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 + saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 17fd35ec9b..de641d479c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -1,9 +1,11 @@ import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -372,7 +374,7 @@ class _DesktopPromptInputState extends State { link: layerLink, child: BlocBuilder( builder: (context, state) { - return PromptInputTextField( + Widget textField = PromptInputTextField( key: textFieldKey, editable: state.editable, cubit: inputControlCubit, @@ -382,6 +384,17 @@ class _DesktopPromptInputState extends State { calculateContentPadding(state.showPredefinedFormats), hintText: state.hintText, ); + + if (!state.editable) { + textField = FlowyTooltip( + message: LocaleKeys + .settings_aiPage_keys_localAINotReadyTextFieldPrompt + .tr(), + child: textField, + ); + } + + return textField; }, ), ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3face24fa5..75463bb225 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -849,11 +849,12 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting...", + "localAIStart": "Local AI is starting. If you find it take too long, you can disable it and enable it again", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is initializing and may take a few minutes, depending on your device", + "localAIInitializing": "Local AI is loading and may take ßa few minutes, depending on your device", + "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c0475aa88f..d5c346db07 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -27,7 +27,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, trace}; +use tracing::{debug, error, info, instrument, trace, warn}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalAISetting { @@ -193,6 +193,14 @@ impl LocalAIController { /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have /// different settings. pub fn is_enabled(&self) -> bool { + if !get_operating_system().is_desktop() { + warn!( + "[AI Plugin] unsupported platform when checking local ai is enabled: {:?}", + get_operating_system() + ); + return false; + } + if let Ok(key) = self .user_service .workspace_id() From aa176f2c126508e3f30ca03ecfa5cf892c6da08e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:14:40 +0800 Subject: [PATCH 148/384] fix(mobile): image formats not shown (#7537) --- .../lib/ai/widgets/prompt_input/predefined_format_buttons.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart index 253d6f70b7..6d6fc8de31 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -48,7 +48,7 @@ class ChangeFormatBar extends StatelessWidget { required this.predefinedFormat, required this.spacing, required this.onSelectPredefinedFormat, - this.showImageFormats = false, + this.showImageFormats = true, }); final PredefinedFormat? predefinedFormat; From 4e1a70c7aca2eaee0b7260248a4f880c46c57ff6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 17:15:12 +0800 Subject: [PATCH 149/384] chore: update local ai crate --- .../setting_ai_view/settings_ai_view.dart | 39 ------------------- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-ai/src/local_ai/controller.rs | 10 +---- 4 files changed, 5 insertions(+), 52 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 65ed04c0df..7102520ac1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -1,11 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -111,39 +108,3 @@ class _AISearchToggle extends StatelessWidget { ); } } - -// ignore: unused_element -class _LocalAIOnBoarding extends StatelessWidget { - const _LocalAIOnBoarding({ - required this.userProfile, - required this.currentWorkspaceMemberRole, - required this.workspaceId, - }); - final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; - final String workspaceId; - - @override - Widget build(BuildContext context) { - if (FeatureFlag.planBilling.isOn) { - return BillingGateGuard( - builder: (context) { - return BlocProvider( - create: (context) => LocalAIOnBoardingBloc( - userProfile, - currentWorkspaceMemberRole, - workspaceId, - )..add(const LocalAIOnBoardingEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return const LocalAISetting(); - }, - ), - ); - }, - ); - } else { - return const SizedBox.shrink(); - } - } -} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 3ed4794177..e687957458 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b1117aa0826be0fed1c543019a9f3187fb99b0e7#b1117aa0826be0fed1c543019a9f3187fb99b0e7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=48271d4a2d225ac0af141b87780bfd07d41ec4f2#48271d4a2d225ac0af141b87780bfd07d41ec4f2" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b1117aa0826be0fed1c543019a9f3187fb99b0e7#b1117aa0826be0fed1c543019a9f3187fb99b0e7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=48271d4a2d225ac0af141b87780bfd07d41ec4f2#48271d4a2d225ac0af141b87780bfd07d41ec4f2" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 9c17eb1d7c..c9c365d7f6 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b1117aa0826be0fed1c543019a9f3187fb99b0e7" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b1117aa0826be0fed1c543019a9f3187fb99b0e7" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "48271d4a2d225ac0af141b87780bfd07d41ec4f2" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "48271d4a2d225ac0af141b87780bfd07d41ec4f2" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index d5c346db07..c0475aa88f 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -27,7 +27,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, trace, warn}; +use tracing::{debug, error, info, instrument, trace}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalAISetting { @@ -193,14 +193,6 @@ impl LocalAIController { /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have /// different settings. pub fn is_enabled(&self) -> bool { - if !get_operating_system().is_desktop() { - warn!( - "[AI Plugin] unsupported platform when checking local ai is enabled: {:?}", - get_operating_system() - ); - return false; - } - if let Ok(key) = self .user_service .workspace_id() From 5b5feb25153df92138689b99acbb92d4aab1a8e3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 20:53:14 +0800 Subject: [PATCH 150/384] chore: disable chat with file --- .../lib/ai/service/ai_prompt_input_bloc.dart | 7 +++---- frontend/rust-lib/flowy-ai/src/chat.rs | 3 ++- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 16ca6bd869..178265b20a 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -40,9 +40,8 @@ class AIPromptInputBloc extends Bloc { event.when( updateAIState: (localAIState) { final aiType = localAIState.enabled ? AiType.local : AiType.cloud; - final supportChatWithFile = - aiType.isLocal && localAIState.state == RunningStatePB.Running; - + // final supportChatWithFile = + // aiType.isLocal && localAIState.state == RunningStatePB.Running; // If local ai is enabled, user can only send messages when the AI is running final editable = localAIState.enabled ? localAIState.state == RunningStatePB.Running @@ -60,7 +59,7 @@ class AIPromptInputBloc extends Bloc { emit( state.copyWith( aiType: aiType, - supportChatWithFile: supportChatWithFile, + supportChatWithFile: false, localAIState: localAIState, editable: editable, hintText: hintText, diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 3156053ae2..bbba58e278 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -115,7 +115,7 @@ impl Chat { &self.chat_id, params.message, params.message_type.clone(), - ¶ms.metadata, + &[], ) .await .map_err(|err| { @@ -126,6 +126,7 @@ impl Chat { let _ = question_sink .send(StreamMessage::MessageId(question.message_id).to_string()) .await; + if let Err(err) = self .chat_service .index_message_metadata(&self.chat_id, ¶ms.metadata, &mut question_sink) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c0475aa88f..d5e86da6d4 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -337,7 +337,6 @@ impl LocalAIController { let mut index_metadata = HashMap::new(); index_metadata.insert("id".to_string(), json!(&metadata.id)); index_metadata.insert("name".to_string(), json!(&metadata.name)); - index_metadata.insert("at_name".to_string(), json!(format!("@{}", &metadata.name))); index_metadata.insert("source".to_string(), json!(&metadata.source)); match &metadata.data.content_type { ContextLoader::Unknown => { From 5fef4f1d499269453a7c492c9300f765d6f1dd14 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 21:51:21 +0800 Subject: [PATCH 151/384] chore: replace guide url --- .../pages/setting_ai_view/init_local_ai.dart | 12 ++++++++---- .../pages/setting_ai_view/ollma_setting.dart | 2 +- .../settings/pages/setting_ai_view/plugin_state.dart | 7 ++++++- frontend/resources/translations/en.json | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart index cf9410dad9..2de410a5e5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -30,10 +30,14 @@ class InitLocalAIIndicator extends StatelessWidget { return Row( children: [ const HSpace(8), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing + .tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + maxLines: 3, + ), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart index bc190b895a..8af4e35914 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart @@ -148,7 +148,7 @@ class _InstallOllamaInstruction extends StatelessWidget { ), recognizer: TapGestureRecognizer() ..onTap = () => afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-ollama", + "https://appflowy.com/guide/appflowy-local-ai-ollama", ), ), TextSpan( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index 0976dc6921..ab9303b429 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -44,7 +44,12 @@ class _PrepareRunning extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStart.tr(), + maxLines: 3, + ), + ), ], ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 75463bb225..624ccecb4a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -849,7 +849,7 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If you find it take too long, you can disable it and enable it again", + "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", From 33d518f3838215d1e02627fd5b99b09f9a6d370f Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 14 Mar 2025 21:52:44 +0800 Subject: [PATCH 152/384] chore: update unsplash client (#7543) --- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++++++---------- frontend/appflowy_flutter/pubspec.lock | 9 ++-- frontend/appflowy_flutter/pubspec.yaml | 5 +++ 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 0cf46a664e..bebb1779c6 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -2285,10 +2285,11 @@ packages: unsplash_client: dependency: "direct main" description: - name: unsplash_client - sha256: "9827f4c1036b7a6ac8cb3f404ac179df7441eee69371d9b17f181817fe502fd7" - url: "https://pub.dev" - source: hosted + path: "." + ref: a8411fc + resolved-ref: a8411fcead178834d1f4572f64dc78b059ffa221 + url: "https://github.com/LucasXu0/unsplash_client.git" + source: git version: "2.2.0" url_launcher: dependency: "direct main" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 4d362fd58b..693e29a505 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -230,6 +230,11 @@ dependency_overrides: path: packages/auto_updater_platform_interface ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458 + unsplash_client: + git: + url: https://github.com/LucasXu0/unsplash_client.git + ref: a8411fc + # auto_updater: # path: /Users/lucas.xu/Desktop/auto_updater/packages/auto_updater From af0c80248642e19a76567cd4f4ba5f0b0158fd07 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 16 Mar 2025 09:37:05 +0800 Subject: [PATCH 153/384] chore: local ai embed file --- .../lib/plugins/ai_chat/chat_page.dart | 35 ++++---- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 1 - .../flowy-ai/src/local_ai/controller.rs | 84 ++++++++----------- .../flowy-ai/src/local_ai/resource.rs | 2 + .../src/middleware/chat_service_mw.rs | 9 +- 7 files changed, 65 insertions(+), 74 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 5400297870..ce84f923d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -354,25 +354,32 @@ class _ChatContentPage extends StatelessWidget { BuildContext context, ChatMessageRefSource metadata, ) async { - if (isURL(metadata.name)) { - late Uri uri; - try { - uri = Uri.parse(metadata.name); - // `Uri` identifies `localhost` as a scheme - if (!uri.hasScheme || uri.scheme == 'localhost') { - uri = Uri.parse("http://${metadata.name}"); - await InternetAddress.lookup(uri.host); - } - await launchUrl(uri); - } catch (err) { - Log.error("failed to open url $err"); - } - } else { + // When the source of metatdata is appflowy, which means it is a appflowy page + if (metadata.source == "appflowy") { final sidebarView = await ViewBackendService.getView(metadata.id).toNullable(); if (context.mounted) { openPageFromMessage(context, sidebarView); } + return; + } + + if (metadata.source == "web") { + if (isURL(metadata.name)) { + late Uri uri; + try { + uri = Uri.parse(metadata.name); + // `Uri` identifies `localhost` as a scheme + if (!uri.hasScheme || uri.scheme == 'localhost') { + uri = Uri.parse("http://${metadata.name}"); + await InternetAddress.lookup(uri.host); + } + await launchUrl(uri); + } catch (err) { + Log.error("failed to open url $err"); + } + } + return; } } } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e687957458..91a5eccdce 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=48271d4a2d225ac0af141b87780bfd07d41ec4f2#48271d4a2d225ac0af141b87780bfd07d41ec4f2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900#19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=48271d4a2d225ac0af141b87780bfd07d41ec4f2#48271d4a2d225ac0af141b87780bfd07d41ec4f2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900#19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c9c365d7f6..5322f51f9a 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "48271d4a2d225ac0af141b87780bfd07d41ec4f2" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "48271d4a2d225ac0af141b87780bfd07d41ec4f2" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index be2b2c8bee..96301a07b0 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -376,7 +376,6 @@ impl AIManager { .await?; let chat_setting_store_key = setting_store_key(chat_id); - if let Some(settings) = self .store_preferences .get_object::(&chat_setting_store_key) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index d5e86da6d4..da24b70145 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -27,7 +27,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, trace}; +use tracing::{debug, error, info, instrument}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalAISetting { @@ -315,6 +315,7 @@ impl LocalAIController { Ok(enabled) } + #[instrument(level = "debug", skip_all)] pub async fn index_message_metadata( &self, chat_id: &str, @@ -322,22 +323,27 @@ impl LocalAIController { index_process_sink: &mut (impl Sink + Unpin), ) -> FlowyResult<()> { if !self.is_enabled() { + info!("[AI Plugin] local ai is disabled, skip indexing"); return Ok(()); } for metadata in metadata_list { - if let Err(err) = metadata.data.validate() { - error!( - "[AI Plugin] invalid metadata: {:?}, error: {:?}", - metadata, err - ); - continue; - } + let mut file_metadata = HashMap::new(); + file_metadata.insert("id".to_string(), json!(&metadata.id)); + file_metadata.insert("name".to_string(), json!(&metadata.name)); + file_metadata.insert("source".to_string(), json!(&metadata.source)); + + let file_path = Path::new(&metadata.data.content); + if !file_path.exists() { + return Err( + FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), + ); + } + info!( + "[AI Plugin] embed file: {:?}, with metadata: {:?}", + file_path, file_metadata + ); - let mut index_metadata = HashMap::new(); - index_metadata.insert("id".to_string(), json!(&metadata.id)); - index_metadata.insert("name".to_string(), json!(&metadata.name)); - index_metadata.insert("source".to_string(), json!(&metadata.source)); match &metadata.data.content_type { ContextLoader::Unknown => { error!( @@ -345,35 +351,16 @@ impl LocalAIController { metadata.data.content_type ); }, - ContextLoader::Text | ContextLoader::Markdown => { - trace!("[AI Plugin]: index text: {}", metadata.data.content); + ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { self .process_index_file( chat_id, - None, - Some(metadata.data.content.clone()), - metadata, - &index_metadata, + file_path.to_path_buf(), + &file_metadata, index_process_sink, ) .await?; }, - ContextLoader::PDF => { - trace!("[AI Plugin]: index pdf file: {}", metadata.data.content); - let file_path = Path::new(&metadata.data.content); - if file_path.exists() { - self - .process_index_file( - chat_id, - Some(file_path.to_path_buf()), - None, - metadata, - &index_metadata, - index_process_sink, - ) - .await?; - } - }, } } @@ -383,43 +370,38 @@ impl LocalAIController { async fn process_index_file( &self, chat_id: &str, - file_path: Option, - content: Option, - metadata: &ChatMessageMetadata, + file_path: PathBuf, index_metadata: &HashMap, index_process_sink: &mut (impl Sink + Unpin), ) -> Result<(), FlowyError> { + let file_name = file_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let _ = index_process_sink .send( StreamMessage::StartIndexFile { - file_name: metadata.name.clone(), + file_name: file_name.clone(), } .to_string(), ) .await; let result = self - .embed_file(chat_id, file_path, content, Some(index_metadata.clone())) + .ai_plugin + .embed_file(chat_id, file_path, Some(index_metadata.clone())) .await; match result { Ok(_) => { let _ = index_process_sink - .send( - StreamMessage::EndIndexFile { - file_name: metadata.name.clone(), - } - .to_string(), - ) + .send(StreamMessage::EndIndexFile { file_name }.to_string()) .await; }, Err(err) => { let _ = index_process_sink - .send( - StreamMessage::IndexFileError { - file_name: metadata.name.clone(), - } - .to_string(), - ) + .send(StreamMessage::IndexFileError { file_name }.to_string()) .await; error!("[AI Plugin] failed to index file: {:?}", err); }, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index a8ee83a256..489a67f69e 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -264,6 +264,8 @@ impl LocalAIResourceController { Some(llm_setting.ollama_server_url.clone()), )?; + //config = config.with_log_level("debug".to_string()); + if rag_enabled { let resource_dir = self.resource_dir()?; let persist_directory = resource_dir.join("vectorstore"); diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 7ebe5889a7..d03b1d88c2 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -66,14 +66,15 @@ impl AICloudServiceMiddleware { let _ = index_process_sink .send(StreamMessage::IndexStart.to_string()) .await; - - self + let result = self .local_ai .index_message_metadata(chat_id, metadata_list, index_process_sink) - .await?; + .await; let _ = index_process_sink .send(StreamMessage::IndexEnd.to_string()) .await; + + result? } else if let Some(_storage_service) = self.storage_service.upgrade() { // } @@ -312,7 +313,7 @@ impl ChatCloudService for AICloudServiceMiddleware { if self.local_ai.is_running() { self .local_ai - .embed_file(chat_id, Some(file_path.to_path_buf()), None, metadata) + .embed_file(chat_id, file_path.to_path_buf(), metadata) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; Ok(()) From 21bf2968a9c2485e330ac44813a9327aa876d00b Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 16 Mar 2025 12:14:11 +0800 Subject: [PATCH 154/384] chore: set releated question height --- .../presentation/chat_related_question.dart | 39 +++++++++---------- .../document/application/document_bloc.dart | 6 +-- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index 34427479fe..2c09e77050 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -67,28 +66,26 @@ class RelatedQuestionItem extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: question, - child: FlowyButton( - mainAxisAlignment: MainAxisAlignment.start, - text: Flexible( - child: FlowyText( - question, - lineHeight: 1.4, - overflow: TextOverflow.ellipsis, - ), + return FlowyButton( + mainAxisAlignment: MainAxisAlignment.start, + text: Flexible( + child: FlowyText( + question, + lineHeight: 1.4, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - expandText: false, - margin: UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0) - : const EdgeInsets.all(8.0), - leftIcon: FlowySvg( - FlowySvgs.ai_chat_outlined_s, - color: Theme.of(context).colorScheme.primary, - size: const Size.square(16.0), - ), - onTap: () => onQuestionSelected(question), ), + expandText: false, + margin: UniversalPlatform.isMobile + ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0) + : const EdgeInsets.all(8.0), + leftIcon: FlowySvg( + FlowySvgs.ai_chat_outlined_s, + color: Theme.of(context).colorScheme.primary, + size: const Size.square(16.0), + ), + onTap: () => onQuestionSelected(question), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 268863664b..20703659d0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -272,12 +272,12 @@ class DocumentBloc extends Bloc { } if (options.inMemoryUpdate) { - Log.info('skip transaction for in-memory update'); + Log.trace('skip transaction for in-memory update'); return; } if (enableDocumentInternalLog) { - Log.debug( + Log.trace( '[TransactionAdapter] 1. transaction before apply: ${transaction.hashCode}', ); } @@ -289,7 +289,7 @@ class DocumentBloc extends Bloc { await _documentRules.applyRules(value: value); if (enableDocumentInternalLog) { - Log.debug( + Log.trace( '[TransactionAdapter] 4. transaction after apply: ${transaction.hashCode}', ); } From 7971566159387c54c97d0e2fc556d016b5becca6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 16 Mar 2025 19:45:51 +0800 Subject: [PATCH 155/384] chore: bump client api --- .../presentation/message/ai_text_message.dart | 8 ++-- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++++---------- frontend/rust-lib/Cargo.lock | 24 +++++----- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 45 +++++++++++------- frontend/rust-lib/flowy-ai/src/completion.rs | 1 + .../rust-lib/flowy-ai/src/stream_message.rs | 6 +++ 7 files changed, 75 insertions(+), 59 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index 5a73501583..5a55072c17 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -113,7 +113,9 @@ class ChatAIMessageWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AIMarkdownText(markdown: state.text), + AIMarkdownText( + markdown: state.text, + ), if (state.sources.isNotEmpty) SelectionContainer.disabled( child: AIMessageMetadata( @@ -128,26 +130,22 @@ class ChatAIMessageWidget extends StatelessWidget { ); }, onError: (error) { - onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.chat_aiServerUnavailable.tr(), ); }, onAIResponseLimit: () { - onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(), ); }, onAIImageResponseLimit: () { - onStopStream(); return ChatErrorMessageWidget( errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(), ); }, onAIMaxRequired: (message) { - onStopStream(); return ChatErrorMessageWidget( errorMessage: message, ); diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 91a5eccdce..4ed30fa6f7 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "bytes", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "futures-channel", "futures-util", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "async-trait", @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "bincode", "bytes", @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "app-error", "jsonwebtoken", @@ -3609,7 +3609,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "bytes", @@ -6176,7 +6176,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 5322f51f9a..7ef8620dc9 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1634d17dd48488f6b55a7e0b874390343f18ebbf" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1634d17dd48488f6b55a7e0b874390343f18ebbf" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index bbba58e278..2ed7a584d0 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -14,7 +14,7 @@ use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; @@ -134,11 +134,9 @@ impl Chat { { error!("Failed to index file: {}", err); } - let _ = question_sink.send(StreamMessage::Done.to_string()).await; // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; - let format = params.format.clone().map(Into::into).unwrap_or_default(); self.stream_response( @@ -220,8 +218,10 @@ impl Chat { match message { QuestionStreamValue::Answer { value } => { answer_stream_buffer.lock().await.push_str(&value); - // trace!("[Chat] stream answer: {}", value); - if let Err(err) = answer_sink.send(format!("data:{}", value)).await { + if let Err(err) = answer_sink + .send(StreamMessage::OnData(value).to_string()) + .await + { error!("Failed to stream answer via IsolateSink: {}", err); } }, @@ -229,7 +229,9 @@ impl Chat { if let Ok(s) = serde_json::to_string(&value) { // trace!("[Chat] stream metadata: {}", s); answer_stream_buffer.lock().await.set_metadata(value); - let _ = answer_sink.send(format!("metadata:{}", s)).await; + let _ = answer_sink + .send(StreamMessage::Metadata(s).to_string()) + .await; } }, QuestionStreamValue::KeepAlive => { @@ -238,16 +240,23 @@ impl Chat { } }, Err(err) => { - error!("[Chat] failed to stream answer: {}", err); - let _ = answer_sink.send(format!("error:{}", err)).await; - let pb = ChatMessageErrorPB { - chat_id: chat_id.clone(), - error_message: err.to_string(), - }; - chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) - .payload(pb) - .send(); - return Err(err); + if err.code == ErrorCode::RequestTimeout || err.code == ErrorCode::Internal { + error!("[Chat] unexpected stream error: {}", err); + let _ = answer_sink.send(StreamMessage::Done.to_string()).await; + } else { + error!("[Chat] failed to stream answer: {}", err); + let _ = answer_sink + .send(StreamMessage::OnError(err.msg.clone()).to_string()) + .await; + let pb = ChatMessageErrorPB { + chat_id: chat_id.clone(), + error_message: err.to_string(), + }; + chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) + .payload(pb) + .send(); + return Err(err); + } }, } } @@ -269,7 +278,9 @@ impl Chat { .send(format!("LOCAL_AI_NOT_READY:{}", err.msg)) .await; } else { - let _ = answer_sink.send(format!("error:{}", err)).await; + let _ = answer_sink + .send(StreamMessage::OnError(err.msg.clone()).to_string()) + .await; } let pb = ChatMessageErrorPB { diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 0dd145d853..7c5b77e030 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -106,6 +106,7 @@ impl CompletionTask { object_id: self.context.object_id, workspace_id: Some(self.workspace_id.clone()), rag_ids: Some(self.context.rag_ids), + completion_history: None, }), format: self.context.format.map(Into::into).unwrap_or_default(), }; diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs index c507262b85..3e76282aa8 100644 --- a/frontend/rust-lib/flowy-ai/src/stream_message.rs +++ b/frontend/rust-lib/flowy-ai/src/stream_message.rs @@ -5,6 +5,9 @@ pub enum StreamMessage { IndexStart, IndexEnd, Text(String), + OnData(String), + OnError(String), + Metadata(String), Done, StartIndexFile { file_name: String }, EndIndexFile { file_name: String }, @@ -20,7 +23,10 @@ impl Display for StreamMessage { StreamMessage::Text(text) => { write!(f, "data:{}", text) }, + StreamMessage::OnData(message) => write!(f, "data:{message}"), + StreamMessage::OnError(message) => write!(f, "error:{message}"), StreamMessage::Done => write!(f, "done:"), + StreamMessage::Metadata(s) => write!(f, "metadata:{s}"), StreamMessage::StartIndexFile { file_name } => { write!(f, "start_index_file:{}", file_name) }, From 2ea8e831cd3a2735c49895675b4e503d930c88cc Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 17 Mar 2025 09:36:46 +0800 Subject: [PATCH 156/384] fix: the workflow of switching to Local in app (#7540) * fix: unable to redo undo when the selection is null * fix: the workflow of switching to Local in app * fix: text color doesn't work in table cell * fix: test --- .../appflowy_flutter/lib/env/cloud_env.dart | 4 ++ .../setting/cloud/appflowy_cloud_page.dart | 2 + .../presentation/editor_configuration.dart | 5 ++ .../undo_redo/custom_undo_redo_commands.dart | 19 +++++- .../sign_in_screen/mobile_sign_in_screen.dart | 9 ++- .../widgets/anonymous_sign_in_button.dart | 63 +++++++++++++++++++ .../widgets/sign_in_agreement.dart | 5 +- .../widgets/sign_in_anonymous_button.dart | 27 ++++++++ frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++------- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/resources/translations/en.json | 2 +- 12 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 986fab128b..15f3ada42e 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -100,6 +100,10 @@ bool get isAuthEnabled { return false; } +bool get isLocalAuthEnabled { + return currentCloudType().isLocal; +} + /// Determines if AppFlowy Cloud is enabled. bool get isAppFlowyCloudEnabled { return currentCloudType().isAppFlowyCloudEnabled; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart index 24c50f7ae6..02d620e559 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -18,6 +19,7 @@ class AppFlowyCloudPage extends StatelessWidget { ), body: SettingCloud( restartAppFlowy: () async { + await getIt().signOut(); await runAppFlowy(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 3889a7d1d3..67ab383eba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1064,6 +1064,11 @@ TextStyle _buildTextStyleInTableCell( }) { TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan); + textStyle = textStyle.copyWith( + fontFamily: textSpan?.style?.fontFamily, + fontSize: textSpan?.style?.fontSize, + ); + if (node.isInHeaderColumn || node.isInHeaderRow || node.isInBoldColumn || diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart index 9ea6477969..36ea3d2704 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart @@ -1,6 +1,8 @@ import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; /// Undo /// @@ -14,10 +16,15 @@ final CommandShortcutEvent customUndoCommand = CommandShortcutEvent( command: 'ctrl+z', macOSCommand: 'cmd+z', handler: (editorState) { - // if the selection is null, it means the keyboard service is disabled - if (editorState.selection == null) { + final context = editorState.document.root.context; + if (context == null) { return KeyEventResult.ignored; } + final editorContext = context.read(); + if (editorContext.coverTitleFocusNode.hasFocus) { + return KeyEventResult.ignored; + } + EditorNotification.undo().post(); return KeyEventResult.handled; }, @@ -35,9 +42,15 @@ final CommandShortcutEvent customRedoCommand = CommandShortcutEvent( command: 'ctrl+y,ctrl+shift+z', macOSCommand: 'cmd+shift+z', handler: (editorState) { - if (editorState.selection == null) { + final context = editorState.document.root.context; + if (context == null) { return KeyEventResult.ignored; } + final editorContext = context.read(); + if (editorContext.coverTitleFocusNode.hasFocus) { + return KeyEventResult.ignored; + } + EditorNotification.redo().post(); return KeyEventResult.handled; }, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 863aadc49c..2a9a6fe798 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -34,7 +35,9 @@ class MobileSignInScreen extends StatelessWidget { const VSpace(spacing), _buildAppNameText(colorScheme), const VSpace(spacing * 2), - const SignInWithMagicLinkButtons(), + isLocalAuthEnabled + ? const SignInAnonymousButtonV3() + : const SignInWithMagicLinkButtons(), const VSpace(spacing), if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), const VSpace(spacing * 1.5), @@ -118,7 +121,9 @@ class MobileSignInScreen extends StatelessWidget { }, ), const HSpace(24), - const SignInAnonymousButtonV2(), + isLocalAuthEnabled + ? const ChangeCloudModeButton() + : const SignInAnonymousButtonV2(), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart new file mode 100644 index 0000000000..1e5d7fa531 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/anon_user_bloc.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SignInAnonymousButtonV3 extends StatelessWidget { + const SignInAnonymousButtonV3({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, signInState) { + return BlocProvider( + create: (context) => AnonUserBloc() + ..add( + const AnonUserEvent.initial(), + ), + child: BlocListener( + listener: (context, state) async { + if (state.openedAnonUser != null) { + await runAppFlowy(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final text = LocaleKeys.signUp_getStartedText.tr(); + final onTap = state.anonUsers.isEmpty + ? () { + context + .read() + .add(const SignInEvent.signedInAsGuest()); + } + : () { + final bloc = context.read(); + final user = bloc.state.anonUsers.first; + bloc.add(AnonUserEvent.openAnonUser(user)); + }; + return ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 32), + maximumSize: const Size(double.infinity, 38), + ), + onPressed: onTap, + child: FlowyText( + text, + fontSize: 14, + color: Theme.of(context).colorScheme.onPrimary, + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index 7351871b6a..a5e0d9784d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -1,4 +1,5 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; @@ -16,7 +17,9 @@ class SignInAgreement extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: '${LocaleKeys.web_signInAgreement.tr()} ', + text: isLocalAuthEnabled + ? '${LocaleKeys.web_signInLocalAgreement.tr()} ' + : '${LocaleKeys.web_signInAgreement.tr()} ', style: const TextStyle(color: Colors.grey, fontSize: 12), ), TextSpan( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index bce22a714d..7fe4584e97 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; @@ -138,3 +139,29 @@ class SignInAnonymousButtonV2 extends StatelessWidget { ); } } + +class ChangeCloudModeButton extends StatelessWidget { + const ChangeCloudModeButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + 'Cloud', + decoration: TextDecoration.underline, + color: Colors.grey, + fontSize: 12, + ), + onTap: () async { + await useAppFlowyBetaCloudWithURL( + kAppflowyCloudUrl, + AuthenticatorType.appflowyCloud, + ); + await runAppFlowy(); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index bebb1779c6..6a2092876c 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2b8c3af" - resolved-ref: "2b8c3af54802f71cfb9156ea2e824bc57faa3ec7" + ref: ec7350d + resolved-ref: ec7350da7c298639c85e88229033da0c8aae8578 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 693e29a505..f78d776639 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "2b8c3af" + ref: "ec7350d" appflowy_editor_plugins: git: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 624ccecb4a..da50dc2504 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -259,7 +259,6 @@ "allSelected": "All selected" }, "stopTooltip": "Stop generating" - }, "trash": { "text": "Trash", @@ -2762,6 +2761,7 @@ "moreOptions": "More options", "collapse": "Collapse", "signInAgreement": "By clicking \"Continue\" above, you agreed to AppFlowy's", + "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to AppFlowy's", "and": "and", "termOfUse": "Terms", "privacyPolicy": "Privacy Policy", From eddb623fba86f379d5c36be3e61c05bced6c7e09 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:02:42 +0800 Subject: [PATCH 157/384] fix: message hover action flashing (#7552) --- .../message/ai_message_bubble.dart | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index a938c9094f..770fb990b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -217,22 +217,22 @@ class _ChatAIMessageHoverState extends State { setState(() => hoverActionBar = false); } }, - child: Container( - constraints: BoxConstraints( - maxWidth: 784, - maxHeight: DesktopAIChatSizes.messageActionBarIconSize + - DesktopAIChatSizes - .messageHoverActionBarPadding.vertical, - ), + child: SizedBox( + width: 784, + height: DesktopAIChatSizes.messageActionBarIconSize + + DesktopAIChatSizes.messageHoverActionBarPadding.vertical, child: hoverBubble || hoverActionBar || overrideVisibility - ? AIMessageActionBar( - message: widget.message, - showDecoration: true, - onRegenerate: widget.onRegenerate, - onChangeFormat: widget.onChangeFormat, - onOverrideVisibility: (visibility) { - overrideVisibility = visibility; - }, + ? Align( + alignment: AlignmentDirectional.centerStart, + child: AIMessageActionBar( + message: widget.message, + showDecoration: true, + onRegenerate: widget.onRegenerate, + onChangeFormat: widget.onChangeFormat, + onOverrideVisibility: (visibility) { + overrideVisibility = visibility; + }, + ), ) : null, ), From 6d327adb83b63fde8f1a3cca1e22115f83187ee7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 17 Mar 2025 13:03:47 +0800 Subject: [PATCH 158/384] fix: quote block flashes in ai chat page when generating answer (#7553) --- .../message/ai_markdown_text.dart | 32 +++++++++++++++++-- .../quote/quote_block_component.dart | 19 +++++++---- frontend/appflowy_flutter/pubspec.lock | 4 +-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart index e14275ca30..1e7d428263 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart @@ -64,8 +64,12 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { super.didUpdateWidget(oldWidget); if (oldWidget.markdown != widget.markdown) { - editorState.dispose(); - editorState = _parseMarkdown(widget.markdown.trim()); + final editorState = _parseMarkdown( + widget.markdown.trim(), + previousDocument: this.editorState.document, + ); + this.editorState.dispose(); + this.editorState = editorState; scrollController.dispose(); scrollController = EditorScrollController( editorState: editorState, @@ -129,8 +133,30 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { ); } - EditorState _parseMarkdown(String markdown) { + EditorState _parseMarkdown( + String markdown, { + Document? previousDocument, + }) { + // merge the nodes from the previous document with the new document to keep the same node ids final document = customMarkdownToDocument(markdown); + final documentIterator = NodeIterator( + document: document, + startNode: document.root, + ); + if (previousDocument != null) { + final previousDocumentIterator = NodeIterator( + document: previousDocument, + startNode: previousDocument.root, + ); + while ( + documentIterator.moveNext() && previousDocumentIterator.moveNext()) { + final currentNode = documentIterator.current; + final previousNode = previousDocumentIterator.current; + if (currentNode.path.equals(previousNode.path)) { + currentNode.id = previousNode.id; + } + } + } final editorState = EditorState(document: document); return editorState; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart index 76f42c54b1..39ab2c5327 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart @@ -5,6 +5,9 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; +/// In memory cache of the quote block height to avoid flashing when the quote block is updated. +Map _quoteBlockHeightCache = {}; + typedef QuoteBlockIconBuilder = Widget Function( BuildContext context, Node node, @@ -115,7 +118,9 @@ class _QuoteBlockComponentWidgetState extends State @override Node get node => widget.node; - ValueNotifier quoteBlockHeightNotifier = ValueNotifier(0); + late ValueNotifier quoteBlockHeightNotifier = ValueNotifier( + _quoteBlockHeightCache[node.id] ?? 0, + ); StreamSubscription? _transactionSubscription; @@ -266,17 +271,19 @@ class _QuoteBlockComponentWidgetState extends State void _updateQuoteBlockHeight() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final renderObject = layoutBuilderKey.currentContext?.findRenderObject(); + double height = _quoteBlockHeightCache[node.id] ?? 0; if (renderObject != null && renderObject is RenderBox) { if (UniversalPlatform.isMobile) { - quoteBlockHeightNotifier.value = - renderObject.size.height - padding.top; + height = renderObject.size.height - padding.top; } else { - quoteBlockHeightNotifier.value = - renderObject.size.height - padding.top * 2; + height = renderObject.size.height - padding.top * 2; } } else { - quoteBlockHeightNotifier.value = 0; + height = 0; } + + quoteBlockHeightNotifier.value = height; + _quoteBlockHeightCache[node.id] = height; }); } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 6a2092876c..4d580a4fd9 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: ec7350d - resolved-ref: ec7350da7c298639c85e88229033da0c8aae8578 + ref: "5070212" + resolved-ref: "5070212ee0f02182a8acdd760b4d7b42264baec4" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f78d776639..25ce68a289 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "ec7350d" + ref: "5070212" appflowy_editor_plugins: git: From 884046ba3fec6f7c9b5504cee1047a68bcb950ce Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 17 Mar 2025 15:41:54 +0800 Subject: [PATCH 159/384] fix: first added cover might be invisible (#7555) --- .../cover/document_immersive_cover_bloc.dart | 7 ++++++- .../header/document_cover_widget.dart | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart index ee8952dc11..3006fc3104 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart @@ -1,6 +1,7 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -19,9 +20,13 @@ class DocumentImmersiveCoverBloc (event, emit) async { await event.when( initial: () async { + final latestView = await ViewBackendService.getView(view.id); add( DocumentImmersiveCoverEvent.updateCoverAndIcon( - view.cover, + latestView.fold( + (s) => s.cover, + (e) => view.cover, + ), EmojiIconData.fromViewIconPB(view.icon), view.name, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index c4f0491596..7cfd28395a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -197,11 +197,14 @@ class _DocumentCoverWidgetState extends State { ), Padding( padding: EdgeInsets.fromLTRB(offset, 0, offset, 12), - child: MouseRegion( - onEnter: (event) => isCoverTitleHovered.value = true, - onExit: (event) => isCoverTitleHovered.value = false, - child: CoverTitle( - view: widget.view, + child: Visibility( + visible: offset != 0, + child: MouseRegion( + onEnter: (event) => isCoverTitleHovered.value = true, + onExit: (event) => isCoverTitleHovered.value = false, + child: CoverTitle( + view: widget.view, + ), ), ), ), From b65fad6214857acc0b52067a7a3b03a24fa9dc94 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 17 Mar 2025 16:57:02 +0800 Subject: [PATCH 160/384] chore: update template link url (#7557) --- .../presentation/home/menu/sidebar/footer/sidebar_footer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart index 57168b40a5..f8c3a30488 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -60,7 +60,7 @@ class SidebarTemplateButton extends StatelessWidget { FlowySvgs.icon_template_s, ), text: LocaleKeys.template_label.tr(), - onTap: () => afLaunchUrlString('https://appflowy.io/templates'), + onTap: () => afLaunchUrlString('https://appflowy.com/templates'), ); } } From 6dd83675fc6d92bf159e07c15105d53ac68a5e65 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 17 Mar 2025 18:03:23 +0800 Subject: [PATCH 161/384] fix: toolbar launch review issues (#7532) * fix: some launch review issues * fix: some launch review issues * fix: color picker position error * fix: redesign the dropdown arrow and padding * feat: implement toolbar button state * fix: keep custom color not changed * feat: revamp color icon in toolbar * fix: correct toolbar position & add animation for toolbar * fix: ajust toolbar animation parameters * chore: adjust some UI values * fix: keep selection after turn into * fix: hover color on toolbar is wrong in dark mode * fix: toolbar icon color in dark mode --- .../document/document_option_action_test.dart | 6 +- ...cument_with_inline_math_equation_test.dart | 6 +- .../document/presentation/editor_page.dart | 16 ++ .../actions/block_action_option_cubit.dart | 21 +- .../option/turn_into_option_action.dart | 15 +- .../ai/ai_writer_toolbar_item.dart | 5 +- .../ai/operations/ai_writer_entities.dart | 2 +- .../desktop_floating_toolbar.dart | 81 ++++++ .../desktop_toolbar/toolbar_animation.dart | 87 +++++++ .../slash_menu_items/todo_list_item.dart | 2 +- .../toggle/toggle_block_shortcuts.dart | 10 +- .../custom_format_toolbar_items.dart | 18 +- .../custom_hightlight_color_toolbar_item.dart | 238 +++++++++++++---- .../custom_link_toolbar_item.dart | 9 +- .../custom_placeholder_toolbar_item.dart | 48 ++++ .../custom_text_align_toolbar_item.dart | 19 +- .../custom_text_color_toolbar_item.dart | 241 ++++++++++++++---- .../more_option_toolbar_item.dart | 36 ++- .../text_heading_toolbar_item.dart | 165 ++++-------- .../text_suggestions_toolbar_item.dart | 98 +++---- .../document/turn_into/turn_into_test.dart | 15 +- .../resources/flowy_icons/16x/ai_explain.svg | 5 - .../resources/flowy_icons/20x/ai_explain.svg | 4 + .../flowy_icons/20x/toolbar_arrow_down.svg | 4 +- .../flowy_icons/20x/toolbar_text_color.svg | 4 +- .../20x/toolbar_text_highlight.svg | 4 +- frontend/resources/translations/en.json | 9 +- 27 files changed, 805 insertions(+), 363 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart delete mode 100644 frontend/resources/flowy_icons/16x/ai_explain.svg create mode 100644 frontend/resources/flowy_icons/20x/ai_explain.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index cbc634cf02..d19dee8b6d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -81,8 +81,7 @@ void main() { LocaleKeys.document_slashMenu_name_numberedList.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.document_slashMenu_name_todoList.tr(): - TodoListBlockKeys.type, + LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, }; @@ -122,8 +121,7 @@ void main() { LocaleKeys.document_slashMenu_name_numberedList.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.document_slashMenu_name_todoList.tr(): - TodoListBlockKeys.type, + LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, }; diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index c29de9f72f..523319abf5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -42,7 +42,7 @@ void main() { // tap the inline math equation button final inlineMathEquationButton = find.text( - LocaleKeys.editor_mathEquationShortForm.tr(), + LocaleKeys.document_toolbar_equation.tr(), ); await tester.tapButton(inlineMathEquationButton); @@ -108,8 +108,8 @@ void main() { await tester.tapButton(moreOptionButton); // expect to the see the inline math equation button is highlighted expect( - tester.widget(inlineMathEquationButton).color != null, - isTrue, + find.byFlowySvg(FlowySvgs.toolbar_check_m), + findsOneWidget, ); // cancel the format diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 4ed434cfa1..289b299de1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -28,9 +28,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; import 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart'; import 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart'; import 'editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; +import 'editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart'; import 'editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart'; import 'editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart'; import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; @@ -93,14 +95,22 @@ class _AppFlowyEditorPageState extends State final List toolbarItems = [ improveWritingItem, + group0PaddingItem, aiWriterItem, customTextHeadingItem, + buildPaddingPlaceholderItem( + 1, + isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, + ), ...customMarkdownFormatItems, + group1PaddingItem, customTextColorItem, + group1PaddingItem, customHighlightColorItem, customInlineCodeItem, suggestionsItem, customLinkItem, + group4PaddingItem, customTextAlignItem, moreOptionItem, ]; @@ -426,12 +436,18 @@ class _AppFlowyEditorPageState extends State padding: EdgeInsets.symmetric(horizontal: 6), style: FloatingToolbarStyle( backgroundColor: Theme.of(context).cardColor, + toolbarActiveColor: Color(0xffe0f8fd), toolbarElevation: 10, ), items: toolbarItems, decoration: context.getPopoverDecoration( borderRadius: BorderRadius.circular(6), ), + toolbarBuilder: (context, child) => DesktopFloatingToolbar( + editorState: editorState, + child: child, + ), + placeHolderBuilder: (_) => customPlaceholderItem, editorState: editorState, editorScrollController: editorScrollController, textDirection: textDirection, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index f3074781b4..8c2f99cdd5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -259,9 +259,10 @@ class BlockActionOptionCubit extends Cubit { emit(BlockActionOptionState()); // Emit a new state to trigger UI update } - Future turnIntoBlock( + static Future turnIntoBlock( String type, - Node node, { + Node node, + EditorState editorState, { int? level, String? currentViewId, }) async { @@ -287,6 +288,7 @@ class BlockActionOptionCubit extends Cubit { type: toType, selectedNodes: selectedNodes, level: level, + editorState: editorState, )) { return true; } @@ -298,6 +300,7 @@ class BlockActionOptionCubit extends Cubit { selectedNodes: selectedNodes, selection: selection, currentViewId: currentViewId, + editorState: editorState, )) { return true; } @@ -353,7 +356,7 @@ class BlockActionOptionCubit extends Cubit { /// /// Returns the altered [Node] with the delta as the Views' name. /// - Future _handleSubPageNode(Node node, Node subPageNode) async { + static Future _handleSubPageNode(Node node, Node subPageNode) async { if (subPageNode.type != SubPageBlockKeys.type) { return node; } @@ -370,7 +373,7 @@ class BlockActionOptionCubit extends Cubit { /// Returns the [Delta] from a SubPage [Node], where the /// [Delta] is the views' name. /// - Future _deltaFromSubPageNode(Node node) async { + static Future _deltaFromSubPageNode(Node node) async { if (node.type != SubPageBlockKeys.type) { return null; } @@ -400,9 +403,10 @@ class BlockActionOptionCubit extends Cubit { // - paragraph 1 // - paragraph 2 // when turning "Toggle Heading 1" into toggle heading, the bulleted items will be moved into the toggle heading - Future turnIntoSingleToggleHeading({ + static Future turnIntoSingleToggleHeading({ required String type, required List selectedNodes, + required EditorState editorState, int? level, Delta? delta, Selection? afterSelection, @@ -492,11 +496,12 @@ class BlockActionOptionCubit extends Cubit { return true; } - Future turnIntoPage({ + static Future turnIntoPage({ required String type, required List selectedNodes, required Selection selection, required String currentViewId, + required EditorState editorState, }) async { if (type != SubPageBlockKeys.type || selectedNodes.isEmpty) { return false; @@ -552,7 +557,7 @@ class BlockActionOptionCubit extends Cubit { return true; } - Future _extractNameFromNodes(List? nodes) async { + static Future _extractNameFromNodes(List? nodes) async { if (nodes == null || nodes.isEmpty) { return ''; } @@ -602,7 +607,7 @@ class BlockActionOptionCubit extends Cubit { return name.substring(0, name.length > 30 ? 30 : name.length); } - List _extractChildViewIds(List nodes) { + static List _extractChildViewIds(List nodes) { final List viewIds = []; for (final node in nodes) { if (node.type == SubPageBlockKeys.type) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart index c69233e440..aa519b7265 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart @@ -237,12 +237,13 @@ class _TurnInfoButton extends StatelessWidget { leftIcon: FlowySvg(leftIcon), rightIcon: rightIcon, itemHeight: ActionListSizes.itemHeight, - onTap: () => context.read().turnIntoBlock( - type, - node, - level: level, - currentViewId: getIt().latestOpenView?.id, - ), + onTap: () => BlockActionOptionCubit.turnIntoBlock( + type, + node, + context.read().editorState, + level: level, + currentViewId: getIt().latestOpenView?.id, + ), ); } @@ -338,7 +339,7 @@ class _TurnInfoButton extends StatelessWidget { case NumberedListBlockKeys.type: return LocaleKeys.document_slashMenu_name_numberedList.tr(); case TodoListBlockKeys.type: - return LocaleKeys.document_slashMenu_name_todoList.tr(); + return LocaleKeys.editor_checkbox.tr(); case CalloutBlockKeys.type: return LocaleKeys.document_slashMenu_name_callout.tr(); case SubPageBlockKeys.type: diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 3f8307bf13..1e17a0b321 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -120,7 +120,7 @@ class _AiWriterToolbarActionListState extends State { Widget buildChild(BuildContext context) { final iconColor = Theme.of(context).iconTheme.color; final child = FlowyIconButton( - width: 52, + width: 48, height: 32, isSelected: isSelected, hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), @@ -132,9 +132,10 @@ class _AiWriterToolbarActionListState extends State { size: Size.square(20), color: iconColor, ), + HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, - size: Size.square(20), + size: Size(12, 20), color: iconColor, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart index e0cc944b3f..5c1909564e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -90,7 +90,7 @@ enum AiWriterCommand { FlowySvgData get icon => switch (this) { userQuestion => FlowySvgs.ai_sparks_s, - explain => FlowySvgs.ai_explain_s, + explain => FlowySvgs.ai_explain_m, // summarize => FlowySvgs.ai_summarize_s, continueWriting || improveWriting => FlowySvgs.ai_improve_writing_s, fixSpellingAndGrammar => FlowySvgs.ai_fix_spelling_grammar_s, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart new file mode 100644 index 0000000000..ee258a002d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -0,0 +1,81 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +import 'toolbar_animation.dart'; + +class DesktopFloatingToolbar extends StatefulWidget { + const DesktopFloatingToolbar({ + super.key, + required this.editorState, + required this.child, + }); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _DesktopFloatingToolbarState(); +} + +class _DesktopFloatingToolbarState extends State { + EditorState get editorState => widget.editorState; + + _Position? position; + + @override + void initState() { + super.initState(); + final selection = editorState.selection; + if (selection == null || selection.isCollapsed) { + return; + } + final selectionRect = editorState.selectionRects(); + if (selectionRect.isEmpty) return; + position = calculateSelectionMenuOffset(selectionRect.first); + } + + @override + Widget build(BuildContext context) { + if (position == null) return Container(); + return Positioned( + left: position!.left, + top: position!.top, + right: position!.right, + child: ToolbarAnimationWidget( + child: widget.child, + ), + ); + } + + _Position calculateSelectionMenuOffset( + Rect rect, + ) { + final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState); + final menuWidth = isLongMenu ? 650.0 : 420.0; + final editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorSize = editorState.renderBox?.size ?? Size.zero; + final editorRect = editorOffset & editorSize; + final left = rect.left, leftStart = 50; + final top = rect.top < 40 ? rect.bottom + 40 : rect.top - 40; + if (left + menuWidth > editorRect.right) { + return _Position( + editorRect.right - menuWidth, + top, + null, + ); + } else if (rect.left - leftStart > 0) { + return _Position(rect.left - leftStart, top, null); + } else { + return _Position(rect.left, top, null); + } + } +} + +class _Position { + _Position(this.left, this.top, this.right); + + final double? left; + final double? top; + final double? right; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart new file mode 100644 index 0000000000..7598a2b657 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +class ToolbarAnimationWidget extends StatefulWidget { + const ToolbarAnimationWidget({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 150), + this.beginOpacity = 0.0, + this.endOpacity = 1.0, + this.beginScaleFactor = 0.95, + this.endScaleFactor = 1.0, + }); + + final Widget child; + final Duration duration; + final double beginScaleFactor; + final double endScaleFactor; + final double beginOpacity; + final double endOpacity; + + @override + State createState() => _ToolbarAnimationWidgetState(); +} + +class _ToolbarAnimationWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation fadeAnimation; + late Animation scaleAnimation; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + fadeAnimation = _buildFadeAnimation(); + scaleAnimation = _buildScaleAnimation(); + controller.forward(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (_, child) => Opacity( + opacity: fadeAnimation.value, + child: Transform.scale( + scale: scaleAnimation.value, + child: child, + ), + ), + child: widget.child, + ); + } + + Animation _buildFadeAnimation() { + return Tween( + begin: widget.beginOpacity, + end: widget.endOpacity, + ).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeInOut, + ), + ); + } + + Animation _buildScaleAnimation() { + return Tween( + begin: widget.beginScaleFactor, + end: widget.endScaleFactor, + ).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeInOut, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart index 74ffa7e075..518dccb35e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart @@ -15,7 +15,7 @@ final _keywords = [ ]; final todoListSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_todoList.tr(), + getName: () => LocaleKeys.editor_checkbox.tr(), keywords: _keywords, handler: (editorState, _, __) async => insertCheckboxAfterSelection( editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart index 212be0f7bf..f3059bf1be 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart @@ -47,15 +47,12 @@ Future _formatGreaterToToggleHeading( delta = delta.compose(Delta()..delete(_greater.length)); // if the previous block is heading block, convert it to toggle heading block if (type == HeadingBlockKeys.type && level != null) { - final cubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - await cubit.turnIntoSingleToggleHeading( + await BlockActionOptionCubit.turnIntoSingleToggleHeading( type: ToggleListBlockKeys.type, selectedNodes: [node], level: level, delta: delta, + editorState: editorState, afterSelection: afterSelection, ); return; @@ -98,7 +95,8 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( } final slicedDelta = delta.slice(selection.start.offset); final transaction = editorState.transaction; - final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool; + final bool collapsed = + node.attributes[ToggleListBlockKeys.collapsed] ?? false; if (collapsed) { // if the delta is empty, clear the format if (delta.isEmpty) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index 68d439ddfe..befd97fd52 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -1,23 +1,27 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; +import 'custom_placeholder_toolbar_item.dart'; + final List customMarkdownFormatItems = [ _FormatToolbarItem( id: 'bold', name: 'bold', svg: FlowySvgs.toolbar_bold_m, ), + group1PaddingItem, _FormatToolbarItem( id: 'underline', name: 'underline', svg: FlowySvgs.toolbar_underline_m, ), + group1PaddingItem, _FormatToolbarItem( id: 'italic', name: 'italic', @@ -57,19 +61,21 @@ class _FormatToolbarItem extends ToolbarItem { delta.everyAttributes((attr) => attr[name] == true), ); - final hoverColor = Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).hoverColor - : AFThemeExtension.of(context).toolbarHoverColor; + final hoverColor = isHighlight + ? highlightColor + : EditorStyleCustomizer.toolbarHoverColor(context); + final isDark = Theme.of(context).brightness == Brightness.dark; final child = FlowyIconButton( width: 36, height: 32, hoverColor: hoverColor, + isSelected: isHighlight, icon: FlowySvg( svg, size: Size.square(20.0), - color: isHighlight - ? highlightColor + color: (isDark && isHighlight) + ? Color(0xFF282E3A) : Theme.of(context).iconTheme.color, ), onPressed: () => editorState.toggleAttribute(name), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 53cc9ce4ca..76407482cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -2,74 +2,218 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; const _kHighlightColorItemId = 'editor.highlightColor'; +String? _customHighlightColorHex; final customHighlightColorItem = ToolbarItem( id: _kHighlightColorItemId, group: 1, isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { - String? highlightColorHex; + builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => + HighlightColorPickerWidget( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + highlightColor: highlightColor, + ), +); - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } +class HighlightColorPickerWidget extends StatefulWidget { + const HighlightColorPickerWidget({ + super.key, + required this.editorState, + this.tooltipBuilder, + required this.highlightColor, + }); - return delta.everyAttributes((attributes) { - highlightColorHex = attributes[AppFlowyRichTextKeys.backgroundColor]; - return highlightColorHex != null; - }); - }); + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + final Color highlightColor; + + @override + State createState() => + _HighlightColorPickerWidgetState(); +} + +class _HighlightColorPickerWidgetState + extends State { + final popoverController = PopoverController(); + + bool isSelected = false; + + EditorState get editorState => widget.editorState; + + Color get highlightColor => widget.highlightColor; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + + @override + Widget build(BuildContext context) { + final selectionRectList = editorState.selectionRects(); + final top = + selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: Offset(0, top), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + margin: EdgeInsets.zero, + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(context), + ); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; final child = FlowyIconButton( width: 36, height: 32, hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_text_highlight_m, - size: Size.square(20.0), - color: isHighlight ? highlightColor : Theme.of(context).iconTheme.color, + icon: SizedBox( + width: 20, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.toolbar_text_highlight_m, + size: Size(20, 16), + color: iconColor, + ), + buildColorfulDivider(iconColor), + ], + ), ), onPressed: () { - bool showClearButton = false; - nodes.allSatisfyInSelection(selection, (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element - .attributes?[AppFlowyRichTextKeys.backgroundColor] != - null; - }, - ); - } - return true; + setState(() { + isSelected = true; }); - showColorMenu( - context, - editorState, - selection, - currentColorHex: highlightColorHex, - isTextColor: false, - showClearButton: showClearButton, - ); + showPopover(); }, ); - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - _kHighlightColorItemId, - AppFlowyEditorL10n.current.highlightColor, - child, + return widget.tooltipBuilder?.call( + context, + _kHighlightColorItemId, + AppFlowyEditorL10n.current.highlightColor, + child, + ) ?? + child; + } + + Widget buildColorfulDivider(Color? iconColor) { + final List colors = []; + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + nodes.allSatisfyInSelection(selection, (delta) { + if (delta.everyAttributes((attr) => attr.isEmpty)) { + return false; + } + + return delta.everyAttributes((attr) { + final textColorHex = attr[AppFlowyRichTextKeys.backgroundColor]; + if (textColorHex != null) colors.add(textColorHex); + return (textColorHex != null); + }); + }); + + final colorLength = colors.length; + if (colors.isEmpty) { + return Container( + width: 20, + height: 4, + color: iconColor, ); } + return SizedBox( + width: 20, + height: 4, + child: Row( + children: List.generate(colorLength, (index) { + final currentColor = int.tryParse(colors[index]); + return Container( + width: 20 / colorLength, + height: 4, + color: currentColor == null ? iconColor : Color(currentColor), + ); + }), + ), + ); + } - return child; - }, -); + Widget buildPopoverContent() { + final List colors = []; + + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + nodes.allSatisfyInSelection(selection, (delta) { + if (delta.everyAttributes((attr) => attr.isEmpty)) { + return false; + } + + return delta.everyAttributes((attributes) { + final highlightColorHex = + attributes[AppFlowyRichTextKeys.backgroundColor]; + if (highlightColorHex != null) colors.add(highlightColorHex); + return highlightColorHex != null; + }); + }); + bool showClearButton = false; + nodes.allSatisfyInSelection(selection, (delta) { + if (!showClearButton) { + showClearButton = delta.whereType().any( + (element) { + return element.attributes?[AppFlowyRichTextKeys.backgroundColor] != + null; + }, + ); + } + return true; + }); + return MouseRegion( + child: ColorPicker( + title: AppFlowyEditorL10n.current.highlightColor, + showClearButton: showClearButton, + selectedColorHex: colors.length == 1 ? colors.first : null, + customColorHex: _customHighlightColorHex, + colorOptions: generateHighlightColorOptions(), + onSubmittedColorHex: (color, isCustomColor) { + if (isCustomColor) { + _customHighlightColorHex = color; + } + formatHighlightColor( + editorState, + editorState.selection, + color, + withUpdateSelection: true, + ); + hidePopover(); + }, + resetText: AppFlowyEditorL10n.current.clearHighlightColor, + resetIconName: 'clear_highlight_color', + ), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + void hidePopover() { + popoverController.close(); + keepEditorFocusNotifier.decrease(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index 048e330f70..f6dc8bce7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -19,14 +19,19 @@ final customLinkItem = ToolbarItem( ); }); + final hoverColor = isHref + ? highlightColor + : EditorStyleCustomizer.toolbarHoverColor(context); + final child = FlowyIconButton( width: 36, height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + hoverColor: hoverColor, + isSelected: isHref, icon: FlowySvg( FlowySvgs.toolbar_link_m, size: Size.square(20.0), - color: isHref ? highlightColor : Theme.of(context).iconTheme.color, + color: Theme.of(context).iconTheme.color, ), onPressed: () => showLinkMenu(context, editorState, selection, isHref), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart new file mode 100644 index 0000000000..b531ecc0a2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +const placeholderItemId = 'editor.placeholder'; +const paddingPlaceholderItemId = 'editor.padding_placeholder'; + +final ToolbarItem customPlaceholderItem = ToolbarItem( + id: placeholderItemId, + group: -1, + isActive: (editorState) => true, + builder: (context, __, ___, ____, _____) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5), + child: Container( + width: 1, + color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 100), + ), + ); + }, +); + +ToolbarItem buildPaddingPlaceholderItem( + int group, { + bool Function(EditorState editorState)? isActive, +}) => + ToolbarItem( + id: paddingPlaceholderItemId, + group: group, + isActive: isActive, + builder: (context, __, ___, ____, _____) => HSpace(4), + ); + +ToolbarItem group0PaddingItem = buildPaddingPlaceholderItem( + 0, + isActive: onlyShowInTextTypeAndExcludeTable, +); + +ToolbarItem group1PaddingItem = + buildPaddingPlaceholderItem(1, isActive: showInAnyTextType); + +ToolbarItem group4PaddingItem = buildPaddingPlaceholderItem( + 4, + isActive: onlyShowInSingleSelectionAndTextType, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index a5f4d59559..3a5cabe6ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -84,7 +84,7 @@ class _TextAlignActionListState extends State { Widget buildChild(BuildContext context) { final iconColor = Theme.of(context).iconTheme.color; final child = FlowyIconButton( - width: 52, + width: 48, height: 32, isSelected: isSelected, hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), @@ -96,9 +96,10 @@ class _TextAlignActionListState extends State { size: Size.square(20), color: iconColor, ), + HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, - size: Size.square(20), + size: Size(12, 20), color: iconColor, ), ], @@ -132,24 +133,20 @@ class _TextAlignActionListState extends State { final isHighlight = nodes.every( (n) => n.attributes[blockComponentAlign] == command.name, ); - final color = - isHighlight ? highlightColor : Theme.of(context).iconTheme.color; return SizedBox( height: 36, child: FlowyButton( leftIconSize: const Size.square(20), - leftIcon: FlowySvg( - command.svg, - color: color, - ), + leftIcon: FlowySvg(command.svg), iconPadding: 12, text: FlowyText( command.title, fontWeight: FontWeight.w400, figmaLineHeight: 20, - color: color, ), + rightIcon: + isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () { command.onAlignChanged(editorState); popoverController.close(); @@ -193,6 +190,10 @@ enum TextAlignCommand { blockComponentAlign: name, }, ), + selectionExtraInfo: { + selectionExtraInfoDoNotAttachTextService: true, + selectionExtraInfoDisableFloatingToolbar: true, + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 7cb7f47bfb..37ca253eb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -2,75 +2,216 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; const _kTextColorItemId = 'editor.textColor'; +String? _customColorHex; final customTextColorItem = ToolbarItem( id: _kTextColorItemId, group: 1, isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { - String? textColorHex; + builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => + TextColorPickerWidget( + editorState: editorState, + tooltipBuilder: tooltipBuilder, + highlightColor: highlightColor, + ), +); + +class TextColorPickerWidget extends StatefulWidget { + const TextColorPickerWidget({ + super.key, + required this.editorState, + this.tooltipBuilder, + required this.highlightColor, + }); + + final EditorState editorState; + final ToolbarTooltipBuilder? tooltipBuilder; + final Color highlightColor; + + @override + State createState() => _TextColorPickerWidgetState(); +} + +class _TextColorPickerWidgetState extends State { + final popoverController = PopoverController(); + + bool isSelected = false; + + EditorState get editorState => widget.editorState; + + Color get highlightColor => widget.highlightColor; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + + @override + Widget build(BuildContext context) { + final selectionRectList = editorState.selectionRects(); + final top = + selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: Offset(0, top), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + setState(() { + isSelected = false; + }); + keepEditorFocusNotifier.decrease(); + }, + margin: EdgeInsets.zero, + popupBuilder: (context) => buildPopoverContent(), + child: buildChild(context), + ); + } + + Widget buildChild(BuildContext context) { + final iconColor = Theme.of(context).iconTheme.color; + final child = FlowyIconButton( + width: 36, + height: 32, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: SizedBox( + width: 20, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.toolbar_text_color_m, + size: Size(20, 16), + color: iconColor, + ), + buildColorfulDivider(iconColor), + ], + ), + ), + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + ); + + return widget.tooltipBuilder?.call( + context, + _kTextColorItemId, + AppFlowyEditorL10n.current.textColor, + child, + ) ?? + child; + } + + Widget buildColorfulDivider(Color? iconColor) { + final List colors = []; final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { + nodes.allSatisfyInSelection(selection, (delta) { if (delta.everyAttributes((attr) => attr.isEmpty)) { return false; } return delta.everyAttributes((attr) { - textColorHex = attr[AppFlowyRichTextKeys.textColor]; + final textColorHex = attr[AppFlowyRichTextKeys.textColor]; + if (textColorHex != null) colors.add(textColorHex); return (textColorHex != null); }); }); - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_text_color_m, - size: Size.square(20.0), - color: isHighlight ? highlightColor : Theme.of(context).iconTheme.color, - ), - onPressed: () { - bool showClearButton = false; - nodes.allSatisfyInSelection( - selection, - (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element.attributes?[AppFlowyRichTextKeys.textColor] != - null; - }, - ); - } - return true; - }, - ); - showColorMenu( - context, - editorState, - selection, - currentColorHex: textColorHex, - isTextColor: true, - showClearButton: showClearButton, - ); - }, - ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - _kTextColorItemId, - AppFlowyEditorL10n.current.textColor, - child, + final colorLength = colors.length; + if (colors.isEmpty) { + return Container( + width: 20, + height: 4, + color: iconColor, ); } + return SizedBox( + width: 20, + height: 4, + child: Row( + children: List.generate(colorLength, (index) { + final currentColor = int.tryParse(colors[index]); + return Container( + width: 20 / colorLength, + height: 4, + color: currentColor == null ? iconColor : Color(currentColor), + ); + }), + ), + ); + } - return child; - }, -); + Widget buildPopoverContent() { + bool showClearButton = false; + final List colors = []; + final selection = editorState.selection!; + final nodes = editorState.getNodesInSelection(selection); + nodes.allSatisfyInSelection(selection, (delta) { + if (delta.everyAttributes((attr) => attr.isEmpty)) { + return false; + } + + return delta.everyAttributes((attr) { + final textColorHex = attr[AppFlowyRichTextKeys.textColor]; + if (textColorHex != null) colors.add(textColorHex); + return (textColorHex != null); + }); + }); + nodes.allSatisfyInSelection( + selection, + (delta) { + if (!showClearButton) { + showClearButton = delta.whereType().any( + (element) { + return element.attributes?[AppFlowyRichTextKeys.textColor] != + null; + }, + ); + } + return true; + }, + ); + return MouseRegion( + child: ColorPicker( + title: AppFlowyEditorL10n.current.textColor, + showClearButton: showClearButton, + selectedColorHex: colors.length == 1 ? colors.first : null, + customColorHex: _customColorHex, + colorOptions: generateTextColorOptions(), + onSubmittedColorHex: (color, isCustomColor) { + if (isCustomColor) { + _customColorHex = color; + } + formatFontColor( + editorState, + editorState.selection, + color, + withUpdateSelection: true, + ); + hidePopover(); + }, + resetText: AppFlowyEditorL10n.current.resetToDefaultColor, + resetIconName: 'reset_text_color', + ), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + void hidePopover() { + popoverController.close(); + keepEditorFocusNotifier.decrease(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart index 7a3e072246..8d92b4a4b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -73,7 +73,7 @@ class _MoreOptionActionListState extends State { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(-8.0, 2.0), + offset: const Offset(0, 2.0), onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { @@ -152,6 +152,8 @@ class _MoreOptionActionListState extends State { Widget buildPopoverContent() { final showFormula = onlyShowInSingleSelectionAndTextType(editorState); + final strikethroughColor = getStrikethroughColor(); + final Color? formulaColor = showFormula ? getFormulaColor() : null; return MouseRegion( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, @@ -160,36 +162,39 @@ class _MoreOptionActionListState extends State { buildFontSelector(), buildCommandItem( MoreOptionCommand.strikethrough, - getStrikethroughColor(), + rightIcon: strikethroughColor != null + ? FlowySvg(FlowySvgs.toolbar_check_m) + : null, ), if (showFormula) buildCommandItem( MoreOptionCommand.formula, - getFormulaColor(), + rightIcon: formulaColor != null + ? FlowySvg(FlowySvgs.toolbar_check_m) + : null, ), ], ), ); } - Widget buildCommandItem(MoreOptionCommand command, Color? color) { + Widget buildCommandItem( + MoreOptionCommand command, { + Widget? rightIcon, + }) { + final isFontCommand = command == MoreOptionCommand.font; return SizedBox( height: 36, child: FlowyButton( - key: command == MoreOptionCommand.font - ? kFontFamilyToolbarItemKey - : null, + key: isFontCommand ? kFontFamilyToolbarItemKey : null, leftIconSize: const Size.square(20), - leftIcon: FlowySvg( - command.svg, - color: color, - ), + leftIcon: FlowySvg(command.svg), + rightIcon: rightIcon, iconPadding: 12, text: FlowyText( command.title, figmaLineHeight: 20, fontWeight: FontWeight.w400, - color: color, ), onTap: () { command.onExecute(editorState); @@ -228,7 +233,10 @@ class _MoreOptionActionListState extends State { await editorState .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); }, - child: buildCommandItem(MoreOptionCommand.font, null), + child: buildCommandItem( + MoreOptionCommand.font, + rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), + ), ); } } @@ -249,7 +257,7 @@ enum MoreOptionCommand { case strikethrough: return LocaleKeys.editor_strikethrough.tr(); case formula: - return LocaleKeys.editor_mathEquationShortForm.tr(); + return LocaleKeys.document_toolbar_equation.tr(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 5a4f5e2ce1..4d1ef3d12a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -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/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -78,7 +79,7 @@ class _TextHeadingActionListState extends State { Widget buildChild(BuildContext context) { final iconColor = Theme.of(context).iconTheme.color; final child = FlowyIconButton( - width: 52, + width: 48, height: 32, isSelected: isSelected, hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), @@ -90,9 +91,10 @@ class _TextHeadingActionListState extends State { size: Size.square(20), color: iconColor, ), + HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, - size: Size.square(20), + size: Size(12, 20), color: iconColor, ), ], @@ -115,6 +117,7 @@ class _TextHeadingActionListState extends State { } Widget buildPopoverContent() { + final selectingCommand = getSelectingCommand(); return MouseRegion( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, @@ -132,7 +135,11 @@ class _TextHeadingActionListState extends State { fontWeight: FontWeight.w400, figmaLineHeight: 20, ), + rightIcon: selectingCommand == command + ? FlowySvg(FlowySvgs.toolbar_check_m) + : null, onTap: () { + if (command == selectingCommand) return; command.onExecute(widget.editorState); popoverController.close(); }, @@ -142,6 +149,27 @@ class _TextHeadingActionListState extends State { ), ); } + + TextHeadingCommand? getSelectingCommand() { + final editorState = widget.editorState; + final selection = editorState.selection; + if (selection == null || !selection.isSingle) { + return null; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null || node.delta == null) { + return null; + } + final nodeType = node.type; + if (nodeType == ParagraphBlockKeys.type) return TextHeadingCommand.text; + if (nodeType == HeadingBlockKeys.type) { + final level = node.attributes[HeadingBlockKeys.level] ?? 1; + if (level == 1) return TextHeadingCommand.h1; + if (level == 2) return TextHeadingCommand.h2; + if (level == 3) return TextHeadingCommand.h3; + } + return null; + } } enum TextHeadingCommand { @@ -167,22 +195,37 @@ enum TextHeadingCommand { } } - void onExecute(EditorState editorState) { + void onExecute(EditorState state) { switch (this) { case text: - formatNodeToText(editorState); + formatNodeToText(state); break; case h1: - onHeadingLevelChanged(editorState, 1); + _turnInto(state, 1); break; case h2: - onHeadingLevelChanged(editorState, 2); + _turnInto(state, 2); break; case h3: - onHeadingLevelChanged(editorState, 3); + _turnInto(state, 3); break; } } + + Future _turnInto(EditorState state, int level) async { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + await BlockActionOptionCubit.turnIntoBlock( + HeadingBlockKeys.type, + node, + state, + level: level, + ); + await state.updateSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ); + } } void formatNodeToText(EditorState editorState) { @@ -203,111 +246,3 @@ void formatNodeToText(EditorState editorState) { ), ); } - -Future onHeadingLevelChanged( - EditorState editorState, - int newLevel, -) async { - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!; - final delta = (node.delta ?? Delta()).toJson(); - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - final originLevel = level; - final type = newLevel == originLevel && node.type == HeadingBlockKeys.type - ? ParagraphBlockKeys.type - : HeadingBlockKeys.type; - - if (type == HeadingBlockKeys.type) { - // from paragraph to heading - final newNode = node.copyWith( - type: type, - attributes: { - HeadingBlockKeys.level: newLevel, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ); - final children = node.children.map((child) => child.deepCopy()); - - final transaction = editorState.transaction; - transaction.insertNodes( - selection.start.path.next, - [newNode, ...children], - ); - transaction.deleteNode(node); - await editorState.apply(transaction); - } else { - // from heading to paragraph - await editorState.formatNode( - selection, - (node) => node.copyWith( - type: type, - attributes: { - HeadingBlockKeys.level: newLevel, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ), - ); - } -} - -Future onToggleLevelChanged( - EditorState editorState, - int? newLevel, -) async { - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!; - final delta = (node.delta ?? Delta()).toJson(); - final level = node.attributes[ToggleListBlockKeys.level]; - final originLevel = level; - - final type = newLevel == originLevel && node.type == ToggleListBlockKeys.type - ? ParagraphBlockKeys.type - : ToggleListBlockKeys.type; - - if (type == ToggleListBlockKeys.type) { - // from paragraph to heading - final newNode = node.copyWith( - type: type, - attributes: { - if (newLevel != null) ToggleListBlockKeys.level: newLevel, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ); - final children = node.children.map((child) => child.deepCopy()); - - final transaction = editorState.transaction; - transaction.insertNodes( - selection.start.path.next, - [newNode, ...children], - ); - transaction.deleteNode(node); - await editorState.apply(transaction); - } else { - // from heading to paragraph - await editorState.formatNode( - selection, - (node) => node.copyWith( - type: type, - attributes: { - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index 946d76b9ab..e805689fc8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart' @@ -143,9 +144,10 @@ class _SuggestionsActionListState extends State { fontWeight: FontWeight.w400, figmaLineHeight: 20, ), + HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, - size: Size.square(20), + size: Size(12, 20), color: iconColor, ), ], @@ -263,7 +265,8 @@ class _SuggestionsActionListState extends State { suggestionItems.clear(); turnIntoItems.clear(); for (final item in suggestions) { - if (item.type.group == suggestionType.group) { + if (item.type.group == suggestionType.group && + item.type != suggestionType) { suggestionItems.add(item); } else { turnIntoItems.add(item); @@ -321,138 +324,101 @@ final h1SuggestionItem = SuggestionItem( type: SuggestionType.h1, title: LocaleKeys.document_toolbar_h1.tr(), svg: FlowySvgs.type_h1_m, - onTap: (state) => onHeadingLevelChanged(state, 1), + onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 1), ); final h2SuggestionItem = SuggestionItem( type: SuggestionType.h2, title: LocaleKeys.document_toolbar_h2.tr(), svg: FlowySvgs.type_h2_m, - onTap: (state) => onHeadingLevelChanged(state, 2), + onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 2), ); final h3SuggestionItem = SuggestionItem( type: SuggestionType.h3, title: LocaleKeys.document_toolbar_h3.tr(), svg: FlowySvgs.type_h3_m, - onTap: (state) => onHeadingLevelChanged(state, 3), + onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 3), ); final checkboxSuggestionItem = SuggestionItem( type: SuggestionType.checkbox, title: LocaleKeys.editor_checkbox.tr(), svg: FlowySvgs.type_todo_m, - onTap: (state) { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - final isHighlight = node.type == TodoListBlockKeys.type; - state.formatNode( - selection, - (node) => node.copyWith( - type: isHighlight ? ParagraphBlockKeys.type : TodoListBlockKeys.type, - ), - ); - }, + onTap: (state) => _turnInto(state, TodoListBlockKeys.type), ); final bulletedSuggestionItem = SuggestionItem( type: SuggestionType.bulleted, title: LocaleKeys.editor_bulletedListShortForm.tr(), svg: FlowySvgs.type_bulleted_list_m, - onTap: (state) { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - final isHighlight = node.type == BulletedListBlockKeys.type; - state.formatNode( - selection, - (node) => node.copyWith( - type: - isHighlight ? ParagraphBlockKeys.type : BulletedListBlockKeys.type, - ), - ); - }, + onTap: (state) => _turnInto(state, BulletedListBlockKeys.type), ); final numberedSuggestionItem = SuggestionItem( type: SuggestionType.numbered, title: LocaleKeys.editor_numberedListShortForm.tr(), svg: FlowySvgs.type_numbered_list_m, - onTap: (state) { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - final isHighlight = node.type == NumberedListBlockKeys.type; - state.formatNode( - selection, - (node) => node.copyWith( - type: - isHighlight ? ParagraphBlockKeys.type : NumberedListBlockKeys.type, - ), - ); - }, + onTap: (state) => _turnInto(state, NumberedListBlockKeys.type), ); final toggleSuggestionItem = SuggestionItem( type: SuggestionType.toggle, title: LocaleKeys.editor_toggleListShortForm.tr(), svg: FlowySvgs.type_toggle_list_m, - onTap: (state) => onToggleLevelChanged(state, null), + onTap: (state) => _turnInto(state, ToggleListBlockKeys.type), ); final toggleH1SuggestionItem = SuggestionItem( type: SuggestionType.toggleH1, title: LocaleKeys.editor_toggleHeading1ShortForm.tr(), svg: FlowySvgs.type_toggle_h1_m, - onTap: (state) => onToggleLevelChanged(state, 1), + onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 1), ); final toggleH2SuggestionItem = SuggestionItem( type: SuggestionType.toggleH2, title: LocaleKeys.editor_toggleHeading2ShortForm.tr(), svg: FlowySvgs.type_toggle_h2_m, - onTap: (state) => onToggleLevelChanged(state, 2), + onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 2), ); final toggleH3SuggestionItem = SuggestionItem( type: SuggestionType.toggleH3, title: LocaleKeys.editor_toggleHeading3ShortForm.tr(), svg: FlowySvgs.type_toggle_h3_m, - onTap: (state) => onToggleLevelChanged(state, 3), + onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 3), ); final callOutSuggestionItem = SuggestionItem( type: SuggestionType.callOut, title: LocaleKeys.document_plugins_callout.tr(), svg: FlowySvgs.type_callout_m, - onTap: (state) { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - final isHighlight = node.type == CalloutBlockKeys.type; - state.formatNode( - selection, - (node) => node.copyWith( - type: isHighlight ? ParagraphBlockKeys.type : CalloutBlockKeys.type, - ), - ); - }, + onTap: (state) => _turnInto(state, CalloutBlockKeys.type), ); final quoteSuggestionItem = SuggestionItem( type: SuggestionType.quote, title: LocaleKeys.editor_quote.tr(), svg: FlowySvgs.type_quote_m, - onTap: (state) { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - final isHighlight = node.type == QuoteBlockKeys.type; - state.formatNode( - selection, - (node) => node.copyWith( - type: isHighlight ? ParagraphBlockKeys.type : QuoteBlockKeys.type, - ), - ); - }, + onTap: (state) => _turnInto(state, QuoteBlockKeys.type), ); +Future _turnInto(EditorState state, String type, {int? level}) async { + final selection = state.selection!; + final node = state.getNodeAtPath(selection.start.path)!; + await BlockActionOptionCubit.turnIntoBlock( + type, + node, + state, + level: level, + ); + await state.updateSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ); +} + final suggestions = UnmodifiableListView([ textSuggestionItem, h1SuggestionItem, diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart index adf2857edb..d2432557eb 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart @@ -24,11 +24,6 @@ void main() { void Function(EditorState editorState, Node node)? afterTurnInto, }) async { final editorState = EditorState(document: document); - final cubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - final types = toType == null ? EditorOptionActionType.turnInto.supportTypes : [toType]; @@ -43,7 +38,12 @@ void main() { final node = editorState.getNodeAtPath([0])!; expect(node.type, originalType); - final result = await cubit.turnIntoBlock(type, node, level: level); + final result = await BlockActionOptionCubit.turnIntoBlock( + type, + node, + editorState, + level: level, + ); expect(result, true); final newNode = editorState.getNodeAtPath([0])!; expect(newNode.type, type); @@ -59,9 +59,10 @@ void main() { Selection.collapsed( Position(path: [0]), ); - await cubit.turnIntoBlock( + await BlockActionOptionCubit.turnIntoBlock( originalType, newNode, + editorState, ); expect(result, true); } diff --git a/frontend/resources/flowy_icons/16x/ai_explain.svg b/frontend/resources/flowy_icons/16x/ai_explain.svg deleted file mode 100644 index 173b35b543..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_explain.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/resources/flowy_icons/20x/ai_explain.svg b/frontend/resources/flowy_icons/20x/ai_explain.svg new file mode 100644 index 0000000000..f490472688 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/ai_explain.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg index 6253ec74d4..e6ef664403 100644 --- a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg +++ b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg index f4b396705f..e96b00ac35 100644 --- a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg +++ b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg index 49add4ba87..ab53a64b38 100644 --- a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg +++ b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index da50dc2504..edbe2b07f1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2116,7 +2116,8 @@ "moreOptions": "More Options", "font": "Font", "suggestions": "Suggestions", - "turnInto": "Turn Into" + "turnInto": "Turn Into", + "equation": "Equation" }, "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", @@ -2418,9 +2419,9 @@ "link": "Link", "numberedList": "Numbered list", "numberedListShortForm": "Numbered", - "toggleHeading1ShortForm": "Toggle h1", - "toggleHeading2ShortForm": "Toggle h2", - "toggleHeading3ShortForm": "Toggle h3", + "toggleHeading1ShortForm": "Toggle H1", + "toggleHeading2ShortForm": "Toggle H2", + "toggleHeading3ShortForm": "Toggle H3", "quote": "Quote", "strikethrough": "Strikethrough", "text": "Text", From a8c5c9c34e82e8008488b62fc64cecc30df916f5 Mon Sep 17 00:00:00 2001 From: NavyStack Date: Tue, 18 Mar 2025 11:30:51 +0900 Subject: [PATCH 162/384] chore(i18n): add and fine-tune ko-KR translations (#7548) * feat(translations): reset hard Korean translation Co-authored-by: NavyStack Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com> * i18n(ko-KR): Add new translations and fine-tune existing ones Co-authored-by: NavyStack Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com> --------- Co-authored-by: fvoci Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com> --- frontend/resources/translations/ko-KR.json | 3044 ++++++++++++++++++-- 1 file changed, 2771 insertions(+), 273 deletions(-) diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index ef6c1cc67e..f956327073 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -1,62 +1,117 @@ { "appName": "AppFlowy", - "defaultUsername": "Me", - "welcomeText": "@:appName 에 오신것을 환영합니다", - "githubStarText": "Star on GitHub", + "defaultUsername": "나", + "welcomeText": "@:appName에 오신 것을 환영합니다", + "welcomeTo": "환영합니다", + "githubStarText": "GitHub에서 별표", "subscribeNewsletterText": "뉴스레터 구독", - "letsGoButtonText": "Let's Go", + "letsGoButtonText": "빠른 시작", "title": "제목", - "youCanAlso": "당신은 또한 수", + "youCanAlso": "또한 할 수 있습니다", "and": "그리고", + "failedToOpenUrl": "URL을 열지 못했습니다: {}", "blockActions": { "addBelowTooltip": "아래에 추가하려면 클릭", "addAboveCmd": "Alt+클릭", "addAboveMacCmd": "Option+클릭", - "addAboveTooltip": "위에 추가" + "addAboveTooltip": "위에 추가하려면", + "dragTooltip": "이동하려면 드래그", + "openMenuTooltip": "메뉴를 열려면 클릭" }, "signUp": { - "buttonText": "회원가입", - "title": "@:appName 에 회원가입", + "buttonText": "가입하기", + "title": "@:appName에 가입하기", "getStartedText": "시작하기", - "emptyPasswordError": "비밀번호는 공백일 수 없습니다", - "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", - "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", + "emptyPasswordError": "비밀번호는 비워둘 수 없습니다", + "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", + "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", "alreadyHaveAnAccount": "이미 계정이 있으신가요?", "emailHint": "이메일", "passwordHint": "비밀번호", - "repeatPasswordHint": "비밀번호 재입력" + "repeatPasswordHint": "비밀번호 확인", + "signUpWith": "다음으로 가입:" }, "signIn": { - "loginTitle": "@:appName 에 로그인", + "loginTitle": "@:appName에 로그인", "loginButtonText": "로그인", + "loginStartWithAnonymous": "익명 세션으로 계속", + "continueAnonymousUser": "익명 세션으로 계속", + "anonymous": "익명", "buttonText": "로그인", + "signingInText": "로그인 중...", "forgotPassword": "비밀번호를 잊으셨나요?", "emailHint": "이메일", "passwordHint": "비밀번호", "dontHaveAnAccount": "계정이 없으신가요?", - "createAccount": "계정 생성", - "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", - "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", + "createAccount": "계정 만들기", + "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", + "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", + "syncPromptMessage": "데이터 동기화에는 시간이 걸릴 수 있습니다. 이 페이지를 닫지 마세요", "or": "또는", + "signInWithGoogle": "Google로 계속", + "signInWithGithub": "GitHub로 계속", + "signInWithDiscord": "Discord로 계속", + "signInWithApple": "Apple로 계속", + "continueAnotherWay": "다른 방법으로 계속", + "signUpWithGoogle": "Google로 가입", + "signUpWithGithub": "GitHub로 가입", + "signUpWithDiscord": "Discord로 가입", + "signInWith": "다음으로 계속:", + "signInWithEmail": "이메일로 계속", "signInWithMagicLink": "계속", + "signUpWithMagicLink": "Magic Link로 가입", "pleaseInputYourEmail": "이메일 주소를 입력하세요", "settings": "설정", + "magicLinkSent": "Magic Link가 전송되었습니다!", "invalidEmail": "유효한 이메일 주소를 입력하세요", - "alreadyHaveAnAccount": "이미 계정이 있나요?", + "alreadyHaveAnAccount": "이미 계정이 있으신가요?", "logIn": "로그인", - "generalError": "오류가 발생했습니다. 나중에 다시 시도하세요", - "loginAsGuestButtonText": "시작하다" + "generalError": "문제가 발생했습니다. 나중에 다시 시도하세요", + "limitRateError": "보안상의 이유로, 매 60초마다 한 번씩만 Magic Link를 요청할 수 있습니다", + "magicLinkSentDescription": "Magic Link가 이메일로 전송되었습니다. 링크를 클릭하여 로그인을 완료하세요. 링크는 5분 후에 만료됩니다." }, "workspace": { - "defaultName": "내 워크스페이스", - "create": "워크스페이스 생성", - "hint": "워크스페이스", - "notFoundError": "워크스페이스를 찾을 수 없습니다" + "chooseWorkspace": "작업 공간 선택", + "defaultName": "내 작업 공간", + "create": "작업 공간 생성", + "new": "새 작업 공간", + "importFromNotion": "Notion에서 가져오기", + "learnMore": "자세히 알아보기", + "reset": "작업 공간 재설정", + "renameWorkspace": "작업 공간 이름 변경", + "workspaceNameCannotBeEmpty": "작업 공간 이름은 비워둘 수 없습니다", + "resetWorkspacePrompt": "작업 공간을 재설정하면 모든 페이지와 데이터가 삭제됩니다. 작업 공간을 재설정하시겠습니까? 또는 지원 팀에 문의하여 작업 공간을 복원할 수 있습니다", + "hint": "작업 공간", + "notFoundError": "작업 공간을 찾을 수 없습니다", + "failedToLoad": "문제가 발생했습니다! 작업 공간을 로드하지 못했습니다. @:appName의 모든 열린 인스턴스를 닫고 다시 시도하세요.", + "errorActions": { + "reportIssue": "문제 보고", + "reportIssueOnGithub": "GitHub에서 문제 보고", + "exportLogFiles": "로그 파일 내보내기", + "reachOut": "Discord에서 문의" + }, + "menuTitle": "작업 공간", + "deleteWorkspaceHintText": "작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다.", + "createSuccess": "작업 공간이 성공적으로 생성되었습니다", + "createFailed": "작업 공간 생성 실패", + "createLimitExceeded": "계정에 허용된 최대 작업 공간 수에 도달했습니다. 추가 작업 공간이 필요하면 GitHub에 요청하세요", + "deleteSuccess": "작업 공간이 성공적으로 삭제되었습니다", + "deleteFailed": "작업 공간 삭제 실패", + "openSuccess": "작업 공간이 성공적으로 열렸습니다", + "openFailed": "작업 공간 열기 실패", + "renameSuccess": "작업 공간 이름이 성공적으로 변경되었습니다", + "renameFailed": "작업 공간 이름 변경 실패", + "updateIconSuccess": "작업 공간 아이콘이 성공적으로 업데이트되었습니다", + "updateIconFailed": "작업 공간 아이콘 업데이트 실패", + "cannotDeleteTheOnlyWorkspace": "유일한 작업 공간을 삭제할 수 없습니다", + "fetchWorkspacesFailed": "작업 공간을 가져오지 못했습니다", + "leaveCurrentWorkspace": "작업 공간 나가기", + "leaveCurrentWorkspacePrompt": "현재 작업 공간을 나가시겠습니까?" }, "shareAction": { "buttonText": "공유", - "workInProgress": "Coming soon", - "markdown": "마크다운", + "workInProgress": "곧 출시 예정", + "markdown": "Markdown", "html": "HTML", "clipboard": "클립보드에 복사", "csv": "CSV", @@ -66,352 +121,1405 @@ "publish": "게시", "unPublish": "게시 취소", "visitSite": "사이트 방문", - "exportAsTab": "내보내기", + "exportAsTab": "다음으로 내보내기", "publishTab": "게시", "shareTab": "공유", "publishOnAppFlowy": "AppFlowy에 게시", "shareTabTitle": "협업 초대", "shareTabDescription": "누구와도 쉽게 협업할 수 있습니다", - "copyLinkSuccess": "클립보드에 링크 복사", + "copyLinkSuccess": "링크가 클립보드에 복사되었습니다", "copyShareLink": "공유 링크 복사", - "copyLinkFailed": "클립보드에 링크를 복사하는 데 실패했습니다", - "copyLinkToBlockSuccess": "블록 링크를 클립보드에 복사했습니다", - "copyLinkToBlockFailed": "블록 링크를 클립보드에 복사하는 데 실패했습니다", + "copyLinkFailed": "링크를 클립보드에 복사하지 못했습니다", + "copyLinkToBlockSuccess": "블록 링크가 클립보드에 복사되었습니다", + "copyLinkToBlockFailed": "블록 링크를 클립보드에 복사하지 못했습니다", "manageAllSites": "모든 사이트 관리", "updatePathName": "경로 이름 업데이트" }, "moreAction": { - "small": "작은", + "small": "작게", "medium": "중간", - "large": "크기가 큰", + "large": "크게", "fontSize": "글꼴 크기", "import": "가져오기", - "moreOptions": "추가 옵션" + "moreOptions": "더 많은 옵션", + "wordCount": "단어 수: {}", + "charCount": "문자 수: {}", + "createdAt": "생성일: {}", + "deleteView": "삭제", + "duplicateView": "복제", + "wordCountLabel": "단어 수: ", + "charCountLabel": "문자 수: ", + "createdAtLabel": "생성일: ", + "syncedAtLabel": "동기화됨: ", + "saveAsNewPage": "페이지에 메시지 추가", + "saveAsNewPageDisabled": "사용 가능한 메시지가 없습니다" }, "importPanel": { - "textAndMarkdown": "텍스트 및 마크다운", - "documentFromV010": "v0.1.0의 문서", - "databaseFromV010": "v0.1.0의 데이터베이스", + "textAndMarkdown": "텍스트 & Markdown", + "documentFromV010": "v0.1.0에서 문서 가져오기", + "databaseFromV010": "v0.1.0에서 데이터베이스 가져오기", + "notionZip": "Notion 내보낸 Zip 파일", "csv": "CSV", - "database": "데이터 베이스" + "database": "데이터베이스" + }, + "emojiIconPicker": { + "iconUploader": { + "placeholderLeft": "파일을 드래그 앤 드롭하거나 클릭하여 ", + "placeholderUpload": "업로드", + "placeholderRight": "하거나 이미지 링크를 붙여넣으세요.", + "dropToUpload": "업로드할 파일을 드롭하세요", + "change": "변경" + } }, "disclosureAction": { - "rename": "이름변경", + "rename": "이름 변경", "delete": "삭제", "duplicate": "복제", - "openNewTab": "새 탭에서 열기" + "unfavorite": "즐겨찾기에서 제거", + "favorite": "즐겨찾기에 추가", + "openNewTab": "새 탭에서 열기", + "moveTo": "이동", + "addToFavorites": "즐겨찾기에 추가", + "copyLink": "링크 복사", + "changeIcon": "아이콘 변경", + "collapseAllPages": "모든 하위 페이지 접기", + "movePageTo": "페이지 이동", + "move": "이동", + "lockPage": "페이지 잠금" }, "blankPageTitle": "빈 페이지", - "newPageText": "새로운 페이지", + "newPageText": "새 페이지", + "newDocumentText": "새 문서", + "newGridText": "새 그리드", + "newCalendarText": "새 캘린더", + "newBoardText": "새 보드", "chat": { - "newChat": "AI 대화" + "newChat": "AI 채팅", + "inputMessageHint": "@:appName AI에게 물어보세요", + "inputLocalAIMessageHint": "@:appName 로컬 AI에게 물어보세요", + "unsupportedCloudPrompt": "이 기능은 @:appName Cloud를 사용할 때만 사용할 수 있습니다", + "relatedQuestion": "추천 질문", + "serverUnavailable": "연결이 끊어졌습니다. 인터넷을 확인하고", + "aiServerUnavailable": "AI 서비스가 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.", + "retry": "다시 시도", + "clickToRetry": "다시 시도하려면 클릭", + "regenerateAnswer": "다시 생성", + "question1": "Kanban을 사용하여 작업 관리하는 방법", + "question2": "GTD 방법 설명", + "question3": "Rust를 사용하는 이유", + "question4": "내 주방에 있는 재료로 요리법 만들기", + "question5": "내 페이지에 대한 일러스트레이션 만들기", + "question6": "다가오는 주의 할 일 목록 작성", + "aiMistakePrompt": "AI는 실수를 할 수 있습니다. 중요한 정보를 확인하세요.", + "chatWithFilePrompt": "파일과 채팅하시겠습니까?", + "indexFileSuccess": "파일 색인화 성공", + "inputActionNoPages": "페이지 결과 없음", + "referenceSource": { + "zero": "0개의 출처 발견", + "one": "{count}개의 출처 발견", + "other": "{count}개의 출처 발견" + }, + "clickToMention": "페이지 언급", + "uploadFile": "PDF, 텍스트 또는 마크다운 파일 첨부", + "questionDetail": "안녕하세요 {}! 오늘 어떻게 도와드릴까요?", + "indexingFile": "{} 색인화 중", + "generatingResponse": "응답 생성 중", + "selectSources": "출처 선택", + "currentPage": "현재 페이지", + "sourcesLimitReached": "최대 3개의 최상위 문서와 그 하위 문서만 선택할 수 있습니다", + "sourceUnsupported": "현재 데이터베이스와의 채팅을 지원하지 않습니다", + "regenerate": "다시 시도", + "addToPageButton": "페이지에 메시지 추가", + "addToPageTitle": "메시지 추가...", + "addToNewPage": "새 페이지 만들기", + "addToNewPageName": "\"{}\"에서 추출한 메시지", + "addToNewPageSuccessToast": "메시지가 추가되었습니다", + "openPagePreviewFailedToast": "페이지를 열지 못했습니다", + "changeFormat": { + "actionButton": "형식 변경", + "confirmButton": "이 형식으로 다시 생성", + "textOnly": "텍스트", + "imageOnly": "이미지 전용", + "textAndImage": "텍스트와 이미지", + "text": "단락", + "bullet": "글머리 기호 목록", + "number": "번호 매기기 목록", + "table": "표", + "blankDescription": "응답 형식", + "defaultDescription": "자동 모드", + "textWithImageDescription": "@:chat.changeFormat.text와 이미지", + "numberWithImageDescription": "@:chat.changeFormat.number와 이미지", + "bulletWithImageDescription": "@:chat.changeFormat.bullet와 이미지", + "tableWithImageDescription": "@:chat.changeFormat.table와 이미지" + }, + "selectBanner": { + "saveButton": "추가 ...", + "selectMessages": "메시지 선택", + "nSelected": "{}개 선택됨", + "allSelected": "모두 선택됨" + }, + "stopTooltip": "생성 중지" }, "trash": { "text": "휴지통", - "restoreAll": "모두 복구", + "restoreAll": "모두 복원", + "restore": "복원", "deleteAll": "모두 삭제", "pageHeader": { "fileName": "파일 이름", - "lastModified": "수정날짜", - "created": "생성날짜" + "lastModified": "마지막 수정", + "created": "생성됨" }, "confirmDeleteAll": { - "title": "휴지통의 모든 페이지를 삭제하시겠습니까?", - "caption": "이 작업은 취소할 수 없습니다." + "title": "휴지통의 모든 페이지", + "caption": "휴지통의 모든 항목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." }, "confirmRestoreAll": { - "title": "휴지통의 모든 페이지를 복원하시겠습니까?", - "caption": "이 작업은 취소할 수 없습니다." - } + "title": "휴지통의 모든 페이지 복원", + "caption": "이 작업은 되돌릴 수 없습니다." + }, + "restorePage": { + "title": "복원: {}", + "caption": "이 페이지를 복원하시겠습니까?" + }, + "mobile": { + "actions": "휴지통 작업", + "empty": "휴지통에 페이지나 공간이 없습니다", + "emptyDescription": "필요 없는 항목을 휴지통으로 이동하세요.", + "isDeleted": "삭제됨", + "isRestored": "복원됨" + }, + "confirmDeleteTitle": "이 페이지를 영구적으로 삭제하시겠습니까?" }, "deletePagePrompt": { - "text": "현재 페이지는 휴지통에 있습니다", - "restore": "페이지 복구", - "deletePermanent": "영구 삭제" + "text": "이 페이지는 휴지통에 있습니다", + "restore": "페이지 복원", + "deletePermanent": "영구적으로 삭제", + "deletePermanentDescription": "이 페이지를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." }, "dialogCreatePageNameHint": "페이지 이름", "questionBubble": { - "shortcuts": "바로 가기", - "whatsNew": "새로운 소식", - "help": "도움 및 지원", - "markdown": "가격 인하", + "shortcuts": "단축키", + "whatsNew": "새로운 기능", + "help": "도움말 및 지원", + "markdown": "Markdown", "debug": { "name": "디버그 정보", - "success": "디버그 정보를 클립보드로 복사했습니다.", - "fail": "디버그 정보를 클립보드로 복사할 수 없습니다." + "success": "디버그 정보를 클립보드에 복사했습니다!", + "fail": "디버그 정보를 클립보드에 복사할 수 없습니다" }, "feedback": "피드백" }, "menuAppHeader": { - "addPageTooltip": "하위에 페이지 추가", - "defaultNewPageName": "제목없음", - "renameDialog": "이름변경" + "moreButtonToolTip": "제거, 이름 변경 등...", + "addPageTooltip": "빠르게 페이지 추가", + "defaultNewPageName": "제목 없음", + "renameDialog": "이름 변경", + "pageNameSuffix": "복사본" }, + "noPagesInside": "내부에 페이지가 없습니다", "toolbar": { - "undo": "실행취소", - "redo": "재실행", + "undo": "실행 취소", + "redo": "다시 실행", "bold": "굵게", "italic": "기울임꼴", "underline": "밑줄", "strike": "취소선", "numList": "번호 매기기 목록", "bulletList": "글머리 기호 목록", - "checkList": "작업 목록", + "checkList": "체크리스트", "inlineCode": "인라인 코드", - "quote": "인용구 블록", + "quote": "인용 블록", "header": "헤더", - "highlight": "하이라이트", + "highlight": "강조", "color": "색상", - "addLink": "링크 추가", - "link": "링크" + "addLink": "링크 추가" }, "tooltip": { - "lightMode": "라이트 모드로 변경", - "darkMode": "다크 모드로 변경", + "lightMode": "라이트 모드로 전환", + "darkMode": "다크 모드로 전환", "openAsPage": "페이지로 열기", - "addNewRow": "열 추가", - "openMenu": "메뉴를 여시려면 클릭하세요", - "dragRow": "행을 재정렬하려면 길게 누르세요.", + "addNewRow": "새 행 추가", + "openMenu": "메뉴 열기", + "dragRow": "행 순서 변경", "viewDataBase": "데이터베이스 보기", - "referencePage": "이 {name}은(는) 참조됩니다", - "addBlockBelow": "아래에 블록 추가" + "referencePage": "이 {name}이 참조됨", + "addBlockBelow": "아래에 블록 추가", + "aiGenerate": "생성" }, "sideBar": { "closeSidebar": "사이드바 닫기", - "openSidebar": "사이드바 열기" + "openSidebar": "사이드바 열기", + "expandSidebar": "전체 페이지로 확장", + "personal": "개인", + "private": "비공개", + "workspace": "작업 공간", + "favorites": "즐겨찾기", + "clickToHidePrivate": "비공개 공간 숨기기\n여기에서 만든 페이지는 본인만 볼 수 있습니다", + "clickToHideWorkspace": "작업 공간 숨기기\n여기에서 만든 페이지는 모든 멤버가 볼 수 있습니다", + "clickToHidePersonal": "개인 공간 숨기기", + "clickToHideFavorites": "즐겨찾기 공간 숨기기", + "addAPage": "새 페이지 추가", + "addAPageToPrivate": "비공개 공간에 페이지 추가", + "addAPageToWorkspace": "작업 공간에 페이지 추가", + "recent": "최근", + "today": "오늘", + "thisWeek": "이번 주", + "others": "이전 즐겨찾기", + "earlier": "이전", + "justNow": "방금", + "minutesAgo": "{count}분 전", + "lastViewed": "마지막으로 본", + "favoriteAt": "즐겨찾기한", + "emptyRecent": "최근 페이지 없음", + "emptyRecentDescription": "페이지를 보면 여기에서 쉽게 찾을 수 있습니다.", + "emptyFavorite": "즐겨찾기 페이지 없음", + "emptyFavoriteDescription": "페이지를 즐겨찾기에 추가하면 여기에서 빠르게 접근할 수 있습니다!", + "removePageFromRecent": "최근 항목에서 이 페이지를 제거하시겠습니까?", + "removeSuccess": "성공적으로 제거되었습니다", + "favoriteSpace": "즐겨찾기", + "RecentSpace": "최근", + "Spaces": "공간", + "upgradeToPro": "Pro로 업그레이드", + "upgradeToAIMax": "무제한 AI 잠금 해제", + "storageLimitDialogTitle": "무료 저장 공간이 부족합니다. 무제한 저장 공간을 잠금 해제하려면 업그레이드하세요", + "storageLimitDialogTitleIOS": "무료 저장 공간이 부족합니다.", + "aiResponseLimitTitle": "무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", + "aiResponseLimitDialogTitle": "AI 응답 한도에 도달했습니다", + "aiResponseLimit": "무료 AI 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max 또는 Pro 플랜을 클릭하여 더 많은 AI 응답을 받으세요", + "askOwnerToUpgradeToPro": "작업 공간의 무료 저장 공간이 부족합니다. 작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요", + "askOwnerToUpgradeToProIOS": "작업 공간의 무료 저장 공간이 부족합니다.", + "askOwnerToUpgradeToAIMax": "작업 공간의 무료 AI 응답이 부족합니다. 작업 공간 소유자에게 플랜을 업그레이드하거나 AI 애드온을 구매하도록 요청하세요", + "askOwnerToUpgradeToAIMaxIOS": "작업 공간의 무료 AI 응답이 부족합니다.", + "purchaseAIMax": "작업 공간의 AI 이미지 응답이 부족합니다. 작업 공간 소유자에게 AI Max를 구매하도록 요청하세요", + "aiImageResponseLimit": "AI 이미지 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max를 클릭하여 더 많은 AI 이미지 응답을 받으세요", + "purchaseStorageSpace": "저장 공간 구매", + "singleFileProPlanLimitationDescription": "무료 플랜에서 허용되는 최대 파일 업로드 크기를 초과했습니다. 더 큰 파일을 업로드하려면 Pro 플랜으로 업그레이드하세요", + "purchaseAIResponse": "구매 ", + "askOwnerToUpgradeToLocalAI": "작업 공간 소유자에게 AI On-device를 활성화하도록 요청하세요", + "upgradeToAILocal": "최고의 프라이버시를 위해 로컬 모델을 장치에서 실행", + "upgradeToAILocalDesc": "PDF와 채팅하고, 글쓰기를 개선하고, 로컬 AI를 사용하여 테이블을 자동으로 채우세요" }, "notifications": { "export": { - "markdown": "마크다운으로 노트를 내보냄", + "markdown": "노트를 Markdown으로 내보냈습니다", "path": "Documents/flowy" } }, "contactsPage": { "title": "연락처", - "whatsHappening": "이번주에는 무슨 일이 있나요?", + "whatsHappening": "이번 주에 무슨 일이 있나요?", "addContact": "연락처 추가", - "editContact": "연락처 편집" + "editContact": "연락처 수정" }, "button": { "ok": "확인", + "confirm": "확인", "done": "완료", "cancel": "취소", "signIn": "로그인", "signOut": "로그아웃", "complete": "완료", "save": "저장", - "generate": "생성하다", + "generate": "생성", "esc": "ESC", - "keep": "유지하다", - "tryAgain": "다시 시도하십시오", - "discard": "버리다", - "replace": "바꾸다", + "keep": "유지", + "tryAgain": "다시 시도", + "discard": "버리기", + "replace": "교체", "insertBelow": "아래에 삽입", + "insertAbove": "위에 삽입", "upload": "업로드", - "edit": "편집하다", + "edit": "편집", "delete": "삭제", - "duplicate": "복제하다", - "putback": "다시 집어 넣어", - "share": "공유" + "copy": "복사", + "duplicate": "복제", + "putback": "되돌리기", + "update": "업데이트", + "share": "공유", + "removeFromFavorites": "즐겨찾기에서 제거", + "removeFromRecent": "최근 항목에서 제거", + "addToFavorites": "즐겨찾기에 추가", + "favoriteSuccessfully": "즐겨찾기에 성공적으로 추가되었습니다", + "unfavoriteSuccessfully": "즐겨찾기에서 성공적으로 제거되었습니다", + "duplicateSuccessfully": "성공적으로 복제되었습니다", + "rename": "이름 변경", + "helpCenter": "도움말 센터", + "add": "추가", + "yes": "예", + "no": "아니요", + "clear": "지우기", + "remove": "제거", + "dontRemove": "제거하지 않음", + "copyLink": "링크 복사", + "align": "정렬", + "login": "로그인", + "logout": "로그아웃", + "deleteAccount": "계정 삭제", + "back": "뒤로", + "signInGoogle": "Google로 계속", + "signInGithub": "GitHub로 계속", + "signInDiscord": "Discord로 계속", + "more": "더 보기", + "create": "생성", + "close": "닫기", + "next": "다음", + "previous": "이전", + "submit": "제출", + "download": "다운로드", + "backToHome": "홈으로 돌아가기", + "viewing": "보기", + "editing": "편집 중", + "gotIt": "알겠습니다", + "retry": "다시 시도", + "uploadFailed": "업로드 실패.", + "copyLinkOriginal": "원본 링크 복사" }, "label": { "welcome": "환영합니다!", "firstName": "이름", "middleName": "중간 이름", "lastName": "성", - "stepX": "{X} 단계" + "stepX": "단계 {X}" }, "oAuth": { "err": { - "failedTitle": "계정에 연결을 할 수 없습니다.", - "failedMsg": "브라우저에서 회원가입이 완료되었는지 확인해주세요." + "failedTitle": "계정에 연결할 수 없습니다.", + "failedMsg": "브라우저에서 로그인 프로세스를 완료했는지 확인하세요." }, "google": { - "title": "GOOGLE SIGN-IN", - "instruction1": "구글 연락처를 가져오기 위해서 웹브라우저로 앱을 승인 해야 합니다.", - "instruction2": "아이콘을 클릭 또는 텍스트를 선택해서 이 코드를 클립보드로 복사하세요:", - "instruction3": "웹브라우저로 다음 링크로 가셔서 위 코드를 입력해주세요:", - "instruction4": "가입 완료 후 아래 버튼을 눌러주세요:" + "title": "GOOGLE 로그인", + "instruction1": "Google 연락처를 가져오려면 웹 브라우저를 사용하여 이 애플리케이션을 인증해야 합니다.", + "instruction2": "아이콘을 클릭하거나 텍스트를 선택하여 이 코드를 클립보드에 복사하세요:", + "instruction3": "웹 브라우저에서 다음 링크로 이동하고 위의 코드를 입력하세요:", + "instruction4": "가입을 완료했으면 아래 버튼을 누르세요:" } }, "settings": { "title": "설정", - "accountPage": { - "general": { - "title": "사용자 이름 & 프로필 사진", - "changeProfilePicture": "프로필 사진 변경" + "popupMenuItem": { + "settings": "설정", + "members": "멤버", + "trash": "휴지통", + "helpAndSupport": "도움말 및 지원" + }, + "sites": { + "title": "사이트", + "namespaceTitle": "네임스페이스", + "namespaceDescription": "네임스페이스 및 홈페이지 관리", + "namespaceHeader": "네임스페이스", + "homepageHeader": "홈페이지", + "updateNamespace": "네임스페이스 업데이트", + "removeHomepage": "홈페이지 제거", + "selectHomePage": "페이지 선택", + "clearHomePage": "이 네임스페이스의 홈페이지를 지웁니다", + "customUrl": "맞춤 URL", + "namespace": { + "description": "이 변경 사항은 이 네임스페이스에 라이브로 게시된 모든 페이지에 적용됩니다", + "tooltip": "부적절한 네임스페이스를 제거할 권리를 보유합니다", + "updateExistingNamespace": "기존 네임스페이스 업데이트", + "upgradeToPro": "Pro 플랜으로 업그레이드하여 홈페이지 설정", + "redirectToPayment": "결제 페이지로 리디렉션 중...", + "onlyWorkspaceOwnerCanSetHomePage": "작업 공간 소유자만 홈페이지를 설정할 수 있습니다", + "pleaseAskOwnerToSetHomePage": "작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요" + }, + "publishedPage": { + "title": "모든 게시된 페이지", + "description": "게시된 페이지 관리", + "page": "페이지", + "pathName": "경로 이름", + "date": "게시 날짜", + "emptyHinText": "이 작업 공간에 게시된 페이지가 없습니다", + "noPublishedPages": "게시된 페이지 없음", + "settings": "게시 설정", + "clickToOpenPageInApp": "앱에서 페이지 열기", + "clickToOpenPageInBrowser": "브라우저에서 페이지 열기" + }, + "error": { + "failedToGeneratePaymentLink": "Pro 플랜 결제 링크 생성 실패", + "failedToUpdateNamespace": "네임스페이스 업데이트 실패", + "proPlanLimitation": "네임스페이스를 업데이트하려면 Pro 플랜으로 업그레이드해야 합니다", + "namespaceAlreadyInUse": "네임스페이스가 이미 사용 중입니다. 다른 네임스페이스를 시도하세요", + "invalidNamespace": "잘못된 네임스페이스입니다. 다른 네임스페이스를 시도하세요", + "namespaceLengthAtLeast2Characters": "네임스페이스는 최소 2자 이상이어야 합니다", + "onlyWorkspaceOwnerCanUpdateNamespace": "작업 공간 소유자만 네임스페이스를 업데이트할 수 있습니다", + "onlyWorkspaceOwnerCanRemoveHomepage": "작업 공간 소유자만 홈페이지를 제거할 수 있습니다", + "setHomepageFailed": "홈페이지 설정 실패", + "namespaceTooLong": "네임스페이스가 너무 깁니다. 다른 네임스페이스를 시도하세요", + "namespaceTooShort": "네임스페이스가 너무 짧습니다. 다른 네임스페이스를 시도하세요", + "namespaceIsReserved": "네임스페이스가 예약되어 있습니다. 다른 네임스페이스를 시도하세요", + "updatePathNameFailed": "경로 이름 업데이트 실패", + "removeHomePageFailed": "홈페이지 제거 실패", + "publishNameContainsInvalidCharacters": "경로 이름에 잘못된 문자가 포함되어 있습니다. 다른 이름을 시도하세요", + "publishNameTooShort": "경로 이름이 너무 짧습니다. 다른 이름을 시도하세요", + "publishNameTooLong": "경로 이름이 너무 깁니다. 다른 이름을 시도하세요", + "publishNameAlreadyInUse": "경로 이름이 이미 사용 중입니다. 다른 이름을 시도하세요", + "namespaceContainsInvalidCharacters": "네임스페이스에 잘못된 문자가 포함되어 있습니다. 다른 네임스페이스를 시도하세요", + "publishPermissionDenied": "작업 공간 소유자 또는 페이지 게시자만 게시 설정을 관리할 수 있습니다", + "publishNameCannotBeEmpty": "경로 이름은 비워둘 수 없습니다. 다른 이름을 시도하세요" + }, + "success": { + "namespaceUpdated": "네임스페이스가 성공적으로 업데이트되었습니다", + "setHomepageSuccess": "홈페이지가 성공적으로 설정되었습니다", + "updatePathNameSuccess": "경로 이름이 성공적으로 업데이트되었습니다", + "removeHomePageSuccess": "홈페이지가 성공적으로 제거되었습니다" } }, + "accountPage": { + "menuLabel": "계정 및 앱", + "title": "내 계정", + "general": { + "title": "계정 이름 및 프로필 이미지", + "changeProfilePicture": "프로필 사진 변경" + }, + "email": { + "title": "이메일", + "actions": { + "change": "이메일 변경" + } + }, + "login": { + "title": "계정 로그인", + "loginLabel": "로그인", + "logoutLabel": "로그아웃" + }, + "isUpToDate": "@:appName이 최신 상태입니다!", + "officialVersion": "버전 {version} (공식 빌드)" + }, + "workspacePage": { + "menuLabel": "작업 공간", + "title": "작업 공간", + "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜/시간 형식 및 언어를 사용자 정의합니다.", + "workspaceName": { + "title": "작업 공간 이름" + }, + "workspaceIcon": { + "title": "작업 공간 아이콘", + "description": "작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다." + }, + "appearance": { + "title": "외관", + "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.", + "options": { + "system": "자동", + "light": "라이트", + "dark": "다크" + } + }, + "resetCursorColor": { + "title": "문서 커서 색상 재설정", + "description": "커서 색상을 재설정하시겠습니까?" + }, + "resetSelectionColor": { + "title": "문서 선택 색상 재설정", + "description": "선택 색상을 재설정하시겠습니까?" + }, + "resetWidth": { + "resetSuccess": "문서 너비가 성공적으로 재설정되었습니다" + }, + "theme": { + "title": "테마", + "description": "미리 설정된 테마를 선택하거나 사용자 정의 테마를 업로드하세요.", + "uploadCustomThemeTooltip": "사용자 정의 테마 업로드" + }, + "workspaceFont": { + "title": "작업 공간 글꼴", + "noFontHint": "글꼴을 찾을 수 없습니다. 다른 용어를 시도하세요." + }, + "textDirection": { + "title": "텍스트 방향", + "leftToRight": "왼쪽에서 오른쪽으로", + "rightToLeft": "오른쪽에서 왼쪽으로", + "auto": "자동", + "enableRTLItems": "RTL 도구 모음 항목 활성화" + }, + "layoutDirection": { + "title": "레이아웃 방향", + "leftToRight": "왼쪽에서 오른쪽으로", + "rightToLeft": "오른쪽에서 왼쪽으로" + }, + "dateTime": { + "title": "날짜 및 시간", + "example": "{}에 {} ({})", + "24HourTime": "24시간 형식", + "dateFormat": { + "label": "날짜 형식", + "local": "로컬", + "us": "미국", + "iso": "ISO", + "friendly": "친숙한", + "dmy": "일/월/년" + } + }, + "language": { + "title": "언어" + }, + "deleteWorkspacePrompt": { + "title": "작업 공간 삭제", + "content": "이 작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다." + }, + "leaveWorkspacePrompt": { + "title": "작업 공간 나가기", + "content": "이 작업 공간을 나가시겠습니까? 모든 페이지와 데이터에 대한 액세스를 잃게 됩니다.", + "success": "작업 공간을 성공적으로 나갔습니다.", + "fail": "작업 공간 나가기 실패." + }, + "manageWorkspace": { + "title": "작업 공간 관리", + "leaveWorkspace": "작업 공간 나가기", + "deleteWorkspace": "작업 공간 삭제" + } + }, + "manageDataPage": { + "menuLabel": "데이터 관리", + "title": "데이터 관리", + "description": "로컬 저장소 데이터를 관리하거나 기존 데이터를 @:appName에 가져옵니다.", + "dataStorage": { + "title": "파일 저장 위치", + "tooltip": "파일이 저장되는 위치", + "actions": { + "change": "경로 변경", + "open": "폴더 열기", + "openTooltip": "현재 데이터 폴더 위치 열기", + "copy": "경로 복사", + "copiedHint": "경로가 복사되었습니다!", + "resetTooltip": "기본 위치로 재설정" + }, + "resetDialog": { + "title": "확실합니까?", + "description": "경로를 기본 데이터 위치로 재설정해도 데이터가 삭제되지 않습니다. 현재 데이터를 다시 가져오려면 현재 위치의 경로를 먼저 복사해야 합니다." + } + }, + "importData": { + "title": "데이터 가져오기", + "tooltip": "@:appName 백업/데이터 폴더에서 데이터 가져오기", + "description": "외부 @:appName 데이터 폴더에서 데이터 복사", + "action": "파일 찾아보기" + }, + "encryption": { + "title": "암호화", + "tooltip": "데이터 저장 및 암호화 방법 관리", + "descriptionNoEncryption": "암호화를 켜면 모든 데이터가 암호화됩니다. 이 작업은 되돌릴 수 없습니다.", + "descriptionEncrypted": "데이터가 암호화되었습니다.", + "action": "데이터 암호화", + "dialog": { + "title": "모든 데이터를 암호화하시겠습니까?", + "description": "모든 데이터를 암호화하면 데이터가 안전하고 보호됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?" + } + }, + "cache": { + "title": "캐시 지우기", + "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", + "dialog": { + "title": "캐시 지우기", + "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", + "successHint": "캐시가 지워졌습니다!" + } + }, + "data": { + "fixYourData": "데이터 수정", + "fixButton": "수정", + "fixYourDataDescription": "데이터에 문제가 있는 경우 여기에서 수정할 수 있습니다." + } + }, + "shortcutsPage": { + "menuLabel": "단축키", + "title": "단축키", + "editBindingHint": "새 바인딩 입력", + "searchHint": "검색", + "actions": { + "resetDefault": "기본값으로 재설정" + }, + "errorPage": { + "message": "단축키를 로드하지 못했습니다: {}", + "howToFix": "다시 시도해 보세요. 문제가 계속되면 GitHub에 문의하세요." + }, + "resetDialog": { + "title": "단축키 재설정", + "description": "모든 키 바인딩을 기본값으로 재설정합니다. 나중에 되돌릴 수 없습니다. 계속하시겠습니까?", + "buttonLabel": "재설정" + }, + "conflictDialog": { + "title": "{}가 이미 사용 중입니다", + "descriptionPrefix": "이 키 바인딩은 현재 ", + "descriptionSuffix": "에서 사용 중입니다. 이 키 바인딩을 교체하면 {}에서 제거됩니다.", + "confirmLabel": "계속" + }, + "editTooltip": "키 바인딩 편집을 시작하려면 누르세요", + "keybindings": { + "toggleToDoList": "할 일 목록 전환", + "insertNewParagraphInCodeblock": "새 단락 삽입", + "pasteInCodeblock": "코드 블록에 붙여넣기", + "selectAllCodeblock": "모두 선택", + "indentLineCodeblock": "줄 시작에 두 칸 삽입", + "outdentLineCodeblock": "줄 시작에서 두 칸 삭제", + "twoSpacesCursorCodeblock": "커서 위치에 두 칸 삽입", + "copy": "선택 항목 복사", + "paste": "내용 붙여넣기", + "cut": "선택 항목 잘라내기", + "alignLeft": "텍스트 왼쪽 정렬", + "alignCenter": "텍스트 가운데 정렬", + "alignRight": "텍스트 오른쪽 정렬", + "insertInlineMathEquation": "인라인 수학 방정식 삽입", + "undo": "실행 취소", + "redo": "다시 실행", + "convertToParagraph": "블록을 단락으로 변환", + "backspace": "삭제", + "deleteLeftWord": "왼쪽 단어 삭제", + "deleteLeftSentence": "왼쪽 문장 삭제", + "delete": "오른쪽 문자 삭제", + "deleteMacOS": "왼쪽 문자 삭제", + "deleteRightWord": "오른쪽 단어 삭제", + "moveCursorLeft": "커서를 왼쪽으로 이동", + "moveCursorBeginning": "커서를 시작으로 이동", + "moveCursorLeftWord": "커서를 왼쪽 단어로 이동", + "moveCursorLeftSelect": "선택하고 커서를 왼쪽으로 이동", + "moveCursorBeginSelect": "선택하고 커서를 시작으로 이동", + "moveCursorLeftWordSelect": "선택하고 커서를 왼쪽 단어로 이동", + "moveCursorRight": "커서를 오른쪽으로 이동", + "moveCursorEnd": "커서를 끝으로 이동", + "moveCursorRightWord": "커서를 오른쪽 단어로 이동", + "moveCursorRightSelect": "선택하고 커서를 오른쪽으로 이동", + "moveCursorEndSelect": "선택하고 커서를 끝으로 이동", + "moveCursorRightWordSelect": "선택하고 커서를 오른쪽 단어로 이동", + "moveCursorUp": "커서를 위로 이동", + "moveCursorTopSelect": "선택하고 커서를 위로 이동", + "moveCursorTop": "커서를 위로 이동", + "moveCursorUpSelect": "선택하고 커서를 위로 이동", + "moveCursorBottomSelect": "선택하고 커서를 아래로 이동", + "moveCursorBottom": "커서를 아래로 이동", + "moveCursorDown": "커서를 아래로 이동", + "moveCursorDownSelect": "선택하고 커서를 아래로 이동", + "home": "맨 위로 스크롤", + "end": "맨 아래로 스크롤", + "toggleBold": "굵게 전환", + "toggleItalic": "기울임꼴 전환", + "toggleUnderline": "밑줄 전환", + "toggleStrikethrough": "취소선 전환", + "toggleCode": "인라인 코드 전환", + "toggleHighlight": "강조 전환", + "showLinkMenu": "링크 메뉴 표시", + "openInlineLink": "인라인 링크 열기", + "openLinks": "선택한 모든 링크 열기", + "indent": "들여쓰기", + "outdent": "내어쓰기", + "exit": "편집 종료", + "pageUp": "한 페이지 위로 스크롤", + "pageDown": "한 페이지 아래로 스크롤", + "selectAll": "모두 선택", + "pasteWithoutFormatting": "서식 없이 붙여넣기", + "showEmojiPicker": "이모지 선택기 표시", + "enterInTableCell": "테이블에 줄 바꿈 추가", + "leftInTableCell": "테이블에서 왼쪽 셀로 이동", + "rightInTableCell": "테이블에서 오른쪽 셀로 이동", + "upInTableCell": "테이블에서 위쪽 셀로 이동", + "downInTableCell": "테이블에서 아래쪽 셀로 이동", + "tabInTableCell": "테이블에서 다음 사용 가능한 셀로 이동", + "shiftTabInTableCell": "테이블에서 이전 사용 가능한 셀로 이동", + "backSpaceInTableCell": "셀의 시작 부분에서 멈춤" + }, + "commands": { + "codeBlockNewParagraph": "코드 블록 옆에 새 단락 삽입", + "codeBlockIndentLines": "코드 블록에서 줄 시작에 두 칸 삽입", + "codeBlockOutdentLines": "코드 블록에서 줄 시작에서 두 칸 삭제", + "codeBlockAddTwoSpaces": "코드 블록에서 커서 위치에 두 칸 삽입", + "codeBlockSelectAll": "코드 블록 내의 모든 내용 선택", + "codeBlockPasteText": "코드 블록에 텍스트 붙여넣기", + "textAlignLeft": "텍스트를 왼쪽으로 정렬", + "textAlignCenter": "텍스트를 가운데로 정렬", + "textAlignRight": "텍스트를 오른쪽으로 정렬" + }, + "couldNotLoadErrorMsg": "단축키를 로드할 수 없습니다. 다시 시도하세요", + "couldNotSaveErrorMsg": "단축키를 저장할 수 없습니다. 다시 시도하세요" + }, "aiPage": { + "title": "AI 설정", + "menuLabel": "AI 설정", "keys": { - "localAILoaded": "로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다.", - "localAIStart": "로컬 AI 대화 시작중...", - "localAILoading": "로컬 AI 대화 모델 로딩중...", - "localAIStopped": "로컬 AI가 중단되었습니다", + "enableAISearchTitle": "AI 검색", + "aiSettingsDescription": "선호하는 모델을 선택하여 AppFlowy AI를 구동하세요. 이제 GPT 4-o, Claude 3,5, Llama 3.1 및 Mistral 7B를 포함합니다", + "loginToEnableAIFeature": "AI 기능은 @:appName Cloud에 로그인한 후에만 활성화됩니다. @:appName 계정이 없는 경우 '내 계정'에서 가입하세요", + "llmModel": "언어 모델", + "llmModelType": "언어 모델 유형", + "downloadLLMPrompt": "{} 다운로드", + "downloadAppFlowyOfflineAI": "AI 오프라인 패키지를 다운로드하면 AI가 장치에서 실행됩니다. 계속하시겠습니까?", + "downloadLLMPromptDetail": "{} 로컬 모델을 다운로드하면 최대 {}의 저장 공간이 필요합니다. 계속하시겠습니까?", + "downloadBigFilePrompt": "다운로드 완료까지 약 10분이 소요될 수 있습니다", + "downloadAIModelButton": "다운로드", + "downloadingModel": "다운로드 중", + "localAILoaded": "로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다", + "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", + "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", + "localAIStopped": "로컬 AI가 중지되었습니다", + "localAIRunning": "로컬 AI가 실행 중입니다", + "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", + "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", - "restartLocalAI": "로컬 AI 재시작", - "disableLocalAIDescription": "로컬 AI를 비활성화하시겠습니까?" + "restartLocalAI": "로컬 AI 다시 시작", + "disableLocalAITitle": "로컬 AI 비활성화", + "disableLocalAIDescription": "로컬 AI를 비활성화하시겠습니까?", + "localAIToggleTitle": "로컬 AI를 활성화 또는 비활성화하려면 전환", + "offlineAIInstruction1": "다음을 따르세요", + "offlineAIInstruction2": "지침", + "offlineAIInstruction3": "오프라인 AI를 활성화하려면", + "offlineAIDownload1": "AppFlowy AI를 다운로드하지 않은 경우 먼저", + "offlineAIDownload2": "다운로드", + "offlineAIDownload3": "하세요", + "activeOfflineAI": "활성화됨", + "downloadOfflineAI": "다운로드", + "openModelDirectory": "폴더 열기", + "localAISetupInstruction1": "이 지침을 따르세요", + "localAISetupInstruction2": "지침", + "localAISetupInstruction3": "Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요", + "startLocalAI": "로컬 AI를 시작하는 데 몇 초가 소요될 수 있습니다" } }, "planPage": { + "menuLabel": "플랜", + "title": "가격 플랜", "planUsage": { - "aiOnDeviceToggle": "최고의 개인 정보 보호를 위한 로컬 AI" + "title": "플랜 사용 요약", + "storageLabel": "저장 공간", + "storageUsage": "{} / {} GB", + "unlimitedStorageLabel": "무제한 저장 공간", + "collaboratorsLabel": "멤버", + "collaboratorsUsage": "{} / {}", + "aiResponseLabel": "AI 응답", + "aiResponseUsage": "{} / {}", + "unlimitedAILabel": "무제한 응답", + "proBadge": "Pro", + "aiMaxBadge": "AI Max", + "aiOnDeviceBadge": "Mac용 AI On-device", + "memberProToggle": "더 많은 멤버 및 무제한 AI", + "aiMaxToggle": "무제한 AI 및 고급 모델 액세스", + "aiOnDeviceToggle": "최고의 프라이버시를 위한 로컬 AI", + "aiCredit": { + "title": "@:appName AI 크레딧 추가", + "price": "{}", + "priceDescription": "1,000 크레딧당", + "purchase": "AI 구매", + "info": "작업 공간당 1,000개의 AI 크레딧을 추가하여 더 스마트하고 빠른 결과를 위해 AI를 워크플로에 원활하게 통합하세요:", + "infoItemOne": "데이터베이스당 최대 10,000개의 응답", + "infoItemTwo": "작업 공간당 최대 1,000개의 응답" + }, + "currentPlan": { + "bannerLabel": "현재 플랜", + "freeTitle": "무료", + "proTitle": "Pro", + "teamTitle": "팀", + "freeInfo": "모든 것을 정리하기 위한 최대 2명의 개인용", + "proInfo": "최대 10명의 소규모 팀을 위한 완벽한 솔루션.", + "teamInfo": "모든 생산적이고 잘 조직된 팀을 위한 완벽한 솔루션.", + "upgrade": "플랜 변경", + "canceledInfo": "플랜이 취소되었습니다. {}에 무료 플랜으로 다운그레이드됩니다." + }, + "addons": { + "title": "애드온", + "addLabel": "추가", + "activeLabel": "추가됨", + "aiMax": { + "title": "AI Max", + "description": "무제한 AI 응답 및 고급 AI 모델로 구동되는 50개의 AI 이미지를 매월 제공합니다", + "price": "{}", + "priceInfo": "연간 청구되는 사용자당 월별" + }, + "aiOnDevice": { + "title": "Mac용 AI On-device", + "description": "장치에서 Mistral 7B, LLAMA 3 및 기타 로컬 모델 실행", + "price": "{}", + "priceInfo": "연간 청구되는 사용자당 월별", + "recommend": "M1 이상 권장" + } + }, + "deal": { + "bannerLabel": "새해 할인!", + "title": "팀을 성장시키세요!", + "info": "Pro 및 팀 플랜을 업그레이드하고 10% 할인 혜택을 받으세요! @:appName AI를 포함한 강력한 새로운 기능으로 작업 공간 생산성을 높이세요.", + "viewPlans": "플랜 보기" + } + } + }, + "billingPage": { + "menuLabel": "청구", + "title": "청구", + "plan": { + "title": "플랜", + "freeLabel": "무료", + "proLabel": "Pro", + "planButtonLabel": "플랜 변경", + "billingPeriod": "청구 기간", + "periodButtonLabel": "기간 수정" + }, + "paymentDetails": { + "title": "결제 세부 정보", + "methodLabel": "결제 방법", + "methodButtonLabel": "방법 수정" + }, + "addons": { + "title": "애드온", + "addLabel": "추가", + "removeLabel": "제거", + "renewLabel": "갱신", + "aiMax": { + "label": "AI Max", + "description": "무제한 AI 및 고급 모델 잠금 해제", + "activeDescription": "다음 청구서가 {}에 만료됩니다", + "canceledDescription": "AI Max는 {}까지 사용할 수 있습니다" + }, + "aiOnDevice": { + "label": "Mac용 AI On-device", + "description": "장치에서 무제한 AI 잠금 해제", + "activeDescription": "다음 청구서가 {}에 만료됩니다", + "canceledDescription": "Mac용 AI On-device는 {}까지 사용할 수 있습니다" + }, + "removeDialog": { + "title": "{} 제거", + "description": "{plan}을 제거하시겠습니까? {plan}의 기능과 혜택에 대한 액세스를 즉시 잃게 됩니다." + } + }, + "currentPeriodBadge": "현재", + "changePeriod": "기간 변경", + "planPeriod": "{} 기간", + "monthlyInterval": "월별", + "monthlyPriceInfo": "월별 청구되는 좌석당", + "annualInterval": "연간", + "annualPriceInfo": "연간 청구되는 좌석당" + }, + "comparePlanDialog": { + "title": "플랜 비교 및 선택", + "planFeatures": "플랜\n기능", + "current": "현재", + "actions": { + "upgrade": "업그레이드", + "downgrade": "다운그레이드", + "current": "현재" + }, + "freePlan": { + "title": "무료", + "description": "모든 것을 정리하기 위한 최대 2명의 개인용", + "price": "{}", + "priceInfo": "영원히 무료" + }, + "proPlan": { + "title": "Pro", + "description": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", + "price": "{}", + "priceInfo": "연간 청구되는 사용자당 월별\n\n{} 월별 청구" + }, + "planLabels": { + "itemOne": "작업 공간", + "itemTwo": "멤버", + "itemThree": "저장 공간", + "itemFour": "실시간 협업", + "itemFive": "모바일 앱", + "itemSix": "AI 응답", + "itemSeven": "AI 이미지", + "itemFileUpload": "파일 업로드", + "customNamespace": "맞춤 네임스페이스", + "tooltipSix": "평생 동안 응답 수는 재설정되지 않습니다", + "intelligentSearch": "지능형 검색", + "tooltipSeven": "작업 공간의 URL 일부를 사용자 정의할 수 있습니다", + "customNamespaceTooltip": "맞춤 게시 사이트 URL" + }, + "freeLabels": { + "itemOne": "작업 공간당 청구", + "itemTwo": "최대 2명", + "itemThree": "5 GB", + "itemFour": "예", + "itemFive": "예", + "itemSix": "평생 10회", + "itemSeven": "평생 2회", + "itemFileUpload": "최대 7 MB", + "intelligentSearch": "지능형 검색" + }, + "proLabels": { + "itemOne": "작업 공간당 청구", + "itemTwo": "최대 10명", + "itemThree": "무제한", + "itemFour": "예", + "itemFive": "예", + "itemSix": "무제한", + "itemSeven": "월별 10개 이미지", + "itemFileUpload": "무제한", + "intelligentSearch": "지능형 검색" + }, + "paymentSuccess": { + "title": "이제 {} 플랜을 사용 중입니다!", + "description": "결제가 성공적으로 처리되었으며 플랜이 @:appName {}로 업그레이드되었습니다. 플랜 세부 정보를 플랜 페이지에서 확인할 수 있습니다" + }, + "downgradeDialog": { + "title": "플랜을 다운그레이드하시겠습니까?", + "description": "플랜을 다운그레이드하면 무료 플랜으로 돌아갑니다. 멤버는 이 작업 공간에 대한 액세스를 잃을 수 있으며 저장 공간 제한을 충족하기 위해 공간을 확보해야 할 수 있습니다.", + "downgradeLabel": "플랜 다운그레이드" } }, "cancelSurveyDialog": { + "title": "떠나셔서 아쉽습니다", + "description": "@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.", + "commonOther": "기타", + "otherHint": "여기에 답변을 작성하세요", + "questionOne": { + "question": "@:appName Pro 구독을 취소한 이유는 무엇입니까?", + "answerOne": "비용이 너무 높음", + "answerTwo": "기능이 기대에 미치지 못함", + "answerThree": "더 나은 대안을 찾음", + "answerFour": "비용을 정당화할 만큼 충분히 사용하지 않음", + "answerFive": "서비스 문제 또는 기술적 어려움" + }, + "questionTwo": { + "question": "미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?", + "answerOne": "매우 가능성이 높음", + "answerTwo": "어느 정도 가능성이 있음", + "answerThree": "잘 모르겠음", + "answerFour": "가능성이 낮음", + "answerFive": "매우 가능성이 낮음" + }, "questionThree": { - "answerFour": "로컬 AI 모델에 대한 액세스" + "question": "구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?", + "answerOne": "다중 사용자 협업", + "answerTwo": "더 긴 시간 버전 기록", + "answerThree": "무제한 AI 응답", + "answerFour": "로컬 AI 모델 액세스" + }, + "questionFour": { + "question": "@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?", + "answerOne": "훌륭함", + "answerTwo": "좋음", + "answerThree": "보통", + "answerFour": "평균 이하", + "answerFive": "불만족" } }, + "common": { + "uploadingFile": "파일 업로드 중입니다. 앱을 종료하지 마세요", + "uploadNotionSuccess": "Notion zip 파일이 성공적으로 업로드되었습니다. 가져오기가 완료되면 확인 이메일을 받게 됩니다", + "reset": "재설정" + }, "menu": { - "appearance": "화면", + "appearance": "외관", "language": "언어", "user": "사용자", "files": "파일", - "open": "설정 열기" + "notifications": "알림", + "open": "설정 열기", + "logout": "로그아웃", + "logoutPrompt": "로그아웃하시겠습니까?", + "selfEncryptionLogoutPrompt": "로그아웃하시겠습니까? 암호화 비밀을 복사했는지 확인하세요", + "syncSetting": "동기화 설정", + "cloudSettings": "클라우드 설정", + "enableSync": "동기화 활성화", + "enableSyncLog": "동기화 로그 활성화", + "enableSyncLogWarning": "동기화 문제를 진단하는 데 도움을 주셔서 감사합니다. 이 작업은 문서 편집 내용을 로컬 파일에 기록합니다. 활성화 후 앱을 종료하고 다시 열어야 합니다", + "enableEncrypt": "데이터 암호화", + "cloudURL": "기본 URL", + "webURL": "웹 URL", + "invalidCloudURLScheme": "잘못된 스키마", + "cloudServerType": "클라우드 서버", + "cloudServerTypeTip": "클라우드 서버를 변경한 후 현재 계정에서 로그아웃될 수 있습니다", + "cloudLocal": "로컬", + "cloudAppFlowy": "@:appName Cloud", + "cloudAppFlowySelfHost": "@:appName Cloud 셀프 호스팅", + "appFlowyCloudUrlCanNotBeEmpty": "클라우드 URL은 비워둘 수 없습니다", + "clickToCopy": "클립보드에 복사", + "selfHostStart": "서버가 없는 경우", + "selfHostContent": "문서", + "selfHostEnd": "를 참조하여 셀프 호스팅 서버를 설정하는 방법을 확인하세요", + "pleaseInputValidURL": "유효한 URL을 입력하세요", + "changeUrl": "셀프 호스팅 URL을 {}로 변경", + "cloudURLHint": "서버의 기본 URL을 입력하세요", + "webURLHint": "웹 서버의 기본 URL을 입력하세요", + "cloudWSURL": "웹소켓 URL", + "cloudWSURLHint": "서버의 웹소켓 주소를 입력하세요", + "restartApp": "재시작", + "restartAppTip": "변경 사항을 적용하려면 애플리케이션을 재시작하세요. 현재 계정에서 로그아웃될 수 있습니다.", + "changeServerTip": "서버를 변경한 후 변경 사항을 적용하려면 재시작 버튼을 클릭해야 합니다", + "enableEncryptPrompt": "이 비밀로 데이터를 보호하려면 암호화를 활성화하세요. 안전하게 보관하세요. 활성화 후에는 비활성화할 수 없습니다. 비밀을 잃어버리면 데이터를 복구할 수 없습니다. 복사하려면 클릭하세요", + "inputEncryptPrompt": "암호화 비밀을 입력하세요", + "clickToCopySecret": "비밀을 복사하려면 클릭", + "configServerSetting": "서버 설정 구성", + "configServerGuide": "`빠른 시작`을 선택한 후 `설정`으로 이동하여 \"클라우드 설정\"을 구성하세요.", + "inputTextFieldHint": "비밀", + "historicalUserList": "사용자 로그인 기록", + "historicalUserListTooltip": "이 목록에는 익명 계정이 표시됩니다. 계정을 클릭하여 세부 정보를 확인할 수 있습니다. 익명 계정은 '시작하기' 버튼을 클릭하여 생성됩니다", + "openHistoricalUser": "익명 계정을 열려면 클릭", + "customPathPrompt": "Google Drive와 같은 클라우드 동기화 폴더에 @:appName 데이터 폴더를 저장하면 위험이 발생할 수 있습니다. 이 폴더 내의 데이터베이스에 여러 위치에서 동시에 액세스하거나 수정하면 동기화 충돌 및 데이터 손상이 발생할 수 있습니다", + "importAppFlowyData": "외부 @:appName 폴더에서 데이터 가져오기", + "importingAppFlowyDataTip": "데이터 가져오는 중입니다. 앱을 종료하지 마세요", + "importAppFlowyDataDescription": "외부 @:appName 데이터 폴더에서 데이터를 복사하여 현재 AppFlowy 데이터 폴더에 가져옵니다", + "importSuccess": "성공적으로 @:appName 데이터 폴더를 가져왔습니다", + "importFailed": "@:appName 데이터 폴더 가져오기 실패", + "importGuide": "자세한 내용은 참조 문서를 확인하세요" + }, + "notifications": { + "enableNotifications": { + "label": "알림 활성화", + "hint": "로컬 알림이 나타나지 않도록 하려면 끄세요." + }, + "showNotificationsIcon": { + "label": "알림 아이콘 표시", + "hint": "사이드바에서 알림 아이콘을 숨기려면 끄세요." + }, + "archiveNotifications": { + "allSuccess": "모든 알림이 성공적으로 보관되었습니다", + "success": "알림이 성공적으로 보관되었습니다" + }, + "markAsReadNotifications": { + "allSuccess": "모두 읽음으로 표시되었습니다", + "success": "읽음으로 표시되었습니다" + }, + "action": { + "markAsRead": "읽음으로 표시", + "multipleChoice": "더 선택", + "archive": "보관" + }, + "settings": { + "settings": "설정", + "markAllAsRead": "모두 읽음으로 표시", + "archiveAll": "모두 보관" + }, + "emptyInbox": { + "title": "받은 편지함 비어 있음!", + "description": "알림을 받으려면 알림을 설정하세요." + }, + "emptyUnread": { + "title": "읽지 않은 알림 없음", + "description": "모든 알림을 확인했습니다!" + }, + "emptyArchived": { + "title": "보관된 항목 없음", + "description": "보관된 알림이 여기에 표시됩니다." + }, + "tabs": { + "inbox": "받은 편지함", + "unread": "읽지 않음", + "archived": "보관됨" + }, + "refreshSuccess": "알림이 성공적으로 새로고침되었습니다", + "titles": { + "notifications": "알림", + "reminder": "알림" + } }, "appearance": { + "resetSetting": "재설정", "fontFamily": { - "label": "글꼴 패밀리", - "search": "검색" + "label": "글꼴", + "search": "검색", + "defaultFont": "시스템" }, "themeMode": { "label": "테마 모드", "light": "라이트 모드", "dark": "다크 모드", - "system": "시스템에 적응" + "system": "시스템에 맞춤" + }, + "fontScaleFactor": "글꼴 크기 비율", + "displaySize": "디스플레이 크기", + "documentSettings": { + "cursorColor": "문서 커서 색상", + "selectionColor": "문서 선택 색상", + "width": "문서 너비", + "changeWidth": "변경", + "pickColor": "색상 선택", + "colorShade": "색상 음영", + "opacity": "불투명도", + "hexEmptyError": "16진수 색상은 비워둘 수 없습니다", + "hexLengthError": "16진수 값은 6자리여야 합니다", + "hexInvalidError": "잘못된 16진수 값", + "opacityEmptyError": "불투명도는 비워둘 수 없습니다", + "opacityRangeError": "불투명도는 1에서 100 사이여야 합니다", + "app": "앱", + "flowy": "Flowy", + "apply": "적용" + }, + "layoutDirection": { + "label": "레이아웃 방향", + "hint": "화면의 콘텐츠 흐름을 왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽으로 제어합니다.", + "ltr": "LTR", + "rtl": "RTL" + }, + "textDirection": { + "label": "기본 텍스트 방향", + "hint": "텍스트가 기본적으로 왼쪽에서 시작할지 오른쪽에서 시작할지 지정합니다.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "자동", + "fallback": "레이아웃 방향과 동일" }, "themeUpload": { "button": "업로드", - "description": "아래 버튼을 사용하여 나만의 @:appName 테마를 업로드하세요.", - "loading": "테마를 확인하고 업로드하는 동안 잠시 기다려 주십시오...", - "uploadSuccess": "테마가 성공적으로 업로드되었습니다.", - "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보십시오.", + "uploadTheme": "테마 업로드", + "description": "아래 버튼을 사용하여 사용자 정의 @:appName 테마를 업로드하세요.", + "loading": "테마를 검증하고 업로드하는 동안 기다려주세요...", + "uploadSuccess": "테마가 성공적으로 업로드되었습니다", + "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보세요.", "filePickerDialogTitle": ".flowy_plugin 파일 선택", - "urlUploadFailure": "URL을 열지 못했습니다: {}", - "failure": "업로드된 테마의 형식이 잘못되었습니다." + "urlUploadFailure": "URL을 열지 못했습니다: {}" }, - "theme": "주제", + "theme": "테마", "builtInsLabel": "내장 테마", "pluginsLabel": "플러그인", - "lightLabel": "라이트 모드", - "darkLabel": "다크 모드" + "dateFormat": { + "label": "날짜 형식", + "local": "로컬", + "us": "미국", + "iso": "ISO", + "friendly": "친숙한", + "dmy": "일/월/년" + }, + "timeFormat": { + "label": "시간 형식", + "twelveHour": "12시간 형식", + "twentyFourHour": "24시간 형식" + }, + "showNamingDialogWhenCreatingPage": "페이지 생성 시 이름 지정 대화 상자 표시", + "enableRTLToolbarItems": "RTL 도구 모음 항목 활성화", + "members": { + "title": "멤버 설정", + "inviteMembers": "멤버 초대", + "inviteHint": "이메일로 초대", + "sendInvite": "초대 보내기", + "copyInviteLink": "초대 링크 복사", + "label": "멤버", + "user": "사용자", + "role": "역할", + "removeFromWorkspace": "작업 공간에서 제거", + "removeFromWorkspaceSuccess": "작업 공간에서 성공적으로 제거되었습니다", + "removeFromWorkspaceFailed": "작업 공간에서 제거 실패", + "owner": "소유자", + "guest": "게스트", + "member": "멤버", + "memberHintText": "멤버는 페이지를 읽고 편집할 수 있습니다", + "guestHintText": "게스트는 페이지를 읽고, 반응하고, 댓글을 달 수 있으며, 권한이 있는 특정 페이지를 편집할 수 있습니다.", + "emailInvalidError": "잘못된 이메일입니다. 확인하고 다시 시도하세요", + "emailSent": "이메일이 전송되었습니다. 받은 편지함을 확인하세요", + "members": "멤버", + "membersCount": { + "zero": "{}명의 멤버", + "one": "{}명의 멤버", + "other": "{}명의 멤버" + }, + "inviteFailedDialogTitle": "초대 전송 실패", + "inviteFailedMemberLimit": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 업그레이드하세요.", + "inviteFailedMemberLimitMobile": "작업 공간의 멤버 한도에 도달했습니다.", + "memberLimitExceeded": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 ", + "memberLimitExceededUpgrade": "업그레이드", + "memberLimitExceededPro": "멤버 한도에 도달했습니다. 더 많은 멤버가 필요하면 ", + "memberLimitExceededProContact": "support@appflowy.io에 문의하세요", + "failedToAddMember": "멤버 추가 실패", + "addMemberSuccess": "멤버가 성공적으로 추가되었습니다", + "removeMember": "멤버 제거", + "areYouSureToRemoveMember": "이 멤버를 제거하시겠습니까?", + "inviteMemberSuccess": "초대가 성공적으로 전송되었습니다", + "failedToInviteMember": "멤버 초대 실패", + "workspaceMembersError": "문제가 발생했습니다", + "workspaceMembersErrorDescription": "현재 멤버 목록을 로드할 수 없습니다. 나중에 다시 시도하세요" + } }, "files": { "copy": "복사", "defaultLocation": "파일 및 데이터 저장 위치 읽기", "exportData": "데이터 내보내기", - "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요.", + "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요", "restoreLocation": "@:appName 기본 경로로 복원", "customizeLocation": "다른 폴더 열기", - "restartApp": "변경 사항을 적용하려면 앱을 다시 시작하십시오.", + "restartApp": "변경 사항을 적용하려면 앱을 재시작하세요.", "exportDatabase": "데이터베이스 내보내기", - "selectFiles": "내보낼 파일을 선택하십시오", + "selectFiles": "내보낼 파일 선택", "selectAll": "모두 선택", - "deselectAll": "모두 선택 취소", + "deselectAll": "모두 선택 해제", "createNewFolder": "새 폴더 만들기", - "createNewFolderDesc": "데이터를 저장할 위치를 알려주십시오.", + "createNewFolderDesc": "데이터를 저장할 위치를 알려주세요", "defineWhereYourDataIsStored": "데이터가 저장되는 위치 정의", - "open": "열려 있는", + "open": "열기", "openFolder": "기존 폴더 열기", - "openFolderDesc": "기존 @:appName 폴더에서 읽고 쓰기", + "openFolderDesc": "기존 @:appName 폴더를 읽고 쓰기", "folderHintText": "폴더 이름", "location": "새 폴더 만들기", - "locationDesc": "@:appName 데이터 폴더의 이름을 선택하세요", - "browser": "검색", - "create": "만들다", - "set": "세트", + "locationDesc": "@:appName 데이터 폴더의 이름을 지정하세요", + "browser": "찾아보기", + "create": "생성", + "set": "설정", "folderPath": "폴더를 저장할 경로", - "locationCannotBeEmpty": "경로는 비워둘 수 없습니다.", + "locationCannotBeEmpty": "경로는 비워둘 수 없습니다", "pathCopiedSnackbar": "파일 저장 경로가 클립보드에 복사되었습니다!", "changeLocationTooltips": "데이터 디렉토리 변경", - "change": "변화", + "change": "변경", "openLocationTooltips": "다른 데이터 디렉토리 열기", "openCurrentDataFolder": "현재 데이터 디렉토리 열기", - "recoverLocationTooltips": "@:appName의 기본 데이터 디렉터리로 재설정", - "exportFileSuccess": "파일을 성공적으로 내보냈습니다!", + "recoverLocationTooltips": "@:appName의 기본 데이터 디렉토리로 재설정", + "exportFileSuccess": "파일이 성공적으로 내보내졌습니다!", "exportFileFail": "파일 내보내기 실패!", - "export": "내보내다" + "export": "내보내기", + "clearCache": "캐시 지우기", + "clearCacheDesc": "이미지가 로드되지 않거나 글꼴이 제대로 표시되지 않는 등의 문제가 발생하면 캐시를 지워보세요. 이 작업은 사용자 데이터에는 영향을 미치지 않습니다.", + "areYouSureToClearCache": "캐시를 지우시겠습니까?", + "clearCacheSuccess": "캐시가 성공적으로 지워졌습니다!" }, "user": { "name": "이름", - "selectAnIcon": "아이콘을 선택하세요", - "pleaseInputYourOpenAIKey": "AI 키를 입력하십시오" + "email": "이메일", + "tooltipSelectIcon": "아이콘 선택", + "selectAnIcon": "아이콘 선택", + "pleaseInputYourOpenAIKey": "AI 키를 입력하세요", + "clickToLogout": "현재 사용자 로그아웃" + }, + "mobile": { + "personalInfo": "개인 정보", + "username": "사용자 이름", + "usernameEmptyError": "사용자 이름은 비워둘 수 없습니다", + "about": "정보", + "pushNotifications": "푸시 알림", + "support": "지원", + "joinDiscord": "Discord에 참여", + "privacyPolicy": "개인정보 보호정책", + "userAgreement": "사용자 계약", + "termsAndConditions": "이용 약관", + "userprofileError": "사용자 프로필을 로드하지 못했습니다", + "userprofileErrorDescription": "로그아웃 후 다시 로그인하여 문제가 계속되는지 확인하세요.", + "selectLayout": "레이아웃 선택", + "selectStartingDay": "시작일 선택", + "version": "버전" } }, "grid": { "deleteView": "이 보기를 삭제하시겠습니까?", - "createView": "새로운", + "createView": "새로 만들기", + "title": { + "placeholder": "제목 없음" + }, "settings": { "filter": "필터", - "sort": "종류", + "sort": "정렬", "sortBy": "정렬 기준", "properties": "속성", - "reorderPropertiesTooltip": "드래그하여 속성 재정렬", + "reorderPropertiesTooltip": "속성 순서 변경", "group": "그룹", "addFilter": "필터 추가", "deleteFilter": "필터 삭제", - "filterBy": "필터링 기준...", - "typeAValue": "값을 입력하세요...", - "layout": "공들여 나열한 것", - "databaseLayout": "공들여 나열한 것", - "Properties": "속성" + "filterBy": "필터 기준", + "typeAValue": "값 입력...", + "layout": "레이아웃", + "compactMode": "압축 모드", + "databaseLayout": "레이아웃", + "viewList": { + "zero": "0개의 보기", + "one": "{count}개의 보기", + "other": "{count}개의 보기" + }, + "editView": "보기 편집", + "boardSettings": "보드 설정", + "calendarSettings": "캘린더 설정", + "createView": "새 보기", + "duplicateView": "보기 복제", + "deleteView": "보기 삭제", + "numberOfVisibleFields": "{}개 표시됨" + }, + "filter": { + "empty": "활성 필터 없음", + "addFilter": "필터 추가", + "cannotFindCreatableField": "필터링할 적절한 필드를 찾을 수 없습니다", + "conditon": "조건", + "where": "조건" }, "textFilter": { "contains": "포함", - "doesNotContain": "포함되어 있지 않다", - "endsWith": "로 끝나다", + "doesNotContain": "포함하지 않음", + "endsWith": "끝남", "startWith": "시작", - "is": "~이다", - "isNot": "아니다", - "isEmpty": "비었다", + "is": "일치", + "isNot": "일치하지 않음", + "isEmpty": "비어 있음", "isNotEmpty": "비어 있지 않음", "choicechipPrefix": { - "isNot": "아니다", + "isNot": "일치하지 않음", "startWith": "시작", - "endWith": "로 끝나다", - "isEmpty": "비었다", - "isNotEmpty": "비어있지 않다" + "endWith": "끝남", + "isEmpty": "비어 있음", + "isNotEmpty": "비어 있지 않음" } }, "checkboxFilter": { - "isChecked": "체크", - "isUnchecked": "체크 해제", + "isChecked": "체크됨", + "isUnchecked": "체크되지 않음", "choicechipPrefix": { - "is": "~이다" + "is": "체크됨" } }, "checklistFilter": { - "isComplete": "완료되었습니다", - "isIncomplted": "불완전하다" + "isComplete": "완료됨", + "isIncomplted": "미완료" }, "selectOptionFilter": { - "is": "~이다", - "isNot": "아니다", + "is": "일치", + "isNot": "일치하지 않음", "contains": "포함", - "doesNotContain": "포함되어 있지 않다", - "isEmpty": "비었다", + "doesNotContain": "포함하지 않음", + "isEmpty": "비어 있음", + "isNotEmpty": "비어 있지 않음" + }, + "dateFilter": { + "is": "일치", + "before": "이전", + "after": "이후", + "onOrBefore": "이전 또는 일치", + "onOrAfter": "이후 또는 일치", + "between": "사이", + "empty": "비어 있음", + "notEmpty": "비어 있지 않음", + "startDate": "시작 날짜", + "endDate": "종료 날짜", + "choicechipPrefix": { + "before": "이전", + "after": "이후", + "between": "사이", + "onOrBefore": "이전 또는 일치", + "onOrAfter": "이후 또는 일치", + "isEmpty": "비어 있음", + "isNotEmpty": "비어 있지 않음" + } + }, + "numberFilter": { + "equal": "일치", + "notEqual": "일치하지 않음", + "lessThan": "미만", + "greaterThan": "초과", + "lessThanOrEqualTo": "이하", + "greaterThanOrEqualTo": "이상", + "isEmpty": "비어 있음", "isNotEmpty": "비어 있지 않음" }, "field": { - "hide": "숨기기", - "insertLeft": "왼쪽 삽입", - "insertRight": "오른쪽 삽입", + "label": "속성", + "hide": "속성 숨기기", + "show": "속성 표시", + "insertLeft": "왼쪽에 삽입", + "insertRight": "오른쪽에 삽입", "duplicate": "복제", "delete": "삭제", + "wrapCellContent": "텍스트 줄 바꿈", + "clear": "셀 지우기", + "switchPrimaryFieldTooltip": "기본 필드의 필드 유형을 변경할 수 없습니다", "textFieldName": "텍스트", "checkboxFieldName": "체크박스", "dateFieldName": "날짜", - "updatedAtFieldName": "마지막 수정 시간", - "createdAtFieldName": "만든 시간", + "updatedAtFieldName": "마지막 수정", + "createdAtFieldName": "생성일", "numberFieldName": "숫자", "singleSelectFieldName": "선택", - "multiSelectFieldName": "다중선택", - "urlFieldName": "링크", + "multiSelectFieldName": "다중 선택", + "urlFieldName": "URL", "checklistFieldName": "체크리스트", + "relationFieldName": "관계", + "summaryFieldName": "AI 요약", + "timeFieldName": "시간", + "mediaFieldName": "파일 및 미디어", + "translateFieldName": "AI 번역", + "translateTo": "번역 대상", "numberFormat": "숫자 형식", "dateFormat": "날짜 형식", - "includeTime": "시간 표시", + "includeTime": "시간 포함", + "isRange": "종료 날짜", "dateFormatFriendly": "월 일, 년", "dateFormatISO": "년-월-일", "dateFormatLocal": "월/일/년", @@ -419,181 +1527,559 @@ "dateFormatDayMonthYear": "일/월/년", "timeFormat": "시간 형식", "invalidTimeFormat": "잘못된 형식", - "timeFormatTwelveHour": "12 시간", - "timeFormatTwentyFourHour": "24 시간", + "timeFormatTwelveHour": "12시간", + "timeFormatTwentyFourHour": "24시간", + "clearDate": "날짜 지우기", + "dateTime": "날짜 시간", + "startDateTime": "시작 날짜 시간", + "endDateTime": "종료 날짜 시간", + "failedToLoadDate": "날짜 값을 로드하지 못했습니다", + "selectTime": "시간 선택", + "selectDate": "날짜 선택", + "visibility": "가시성", + "propertyType": "속성 유형", "addSelectOption": "옵션 추가", + "typeANewOption": "새 옵션 입력", "optionTitle": "옵션", "addOption": "옵션 추가", "editProperty": "속성 편집", - "newProperty": "열 추가", - "deleteFieldPromptMessage": "해당 속성을 삭제 하시겠습니까?" + "newProperty": "새 속성", + "openRowDocument": "페이지로 열기", + "deleteFieldPromptMessage": "확실합니까? 이 속성과 모든 데이터가 삭제됩니다", + "clearFieldPromptMessage": "확실합니까? 이 열의 모든 셀이 비워집니다", + "newColumn": "새 열", + "format": "형식", + "reminderOnDateTooltip": "이 셀에는 예약된 알림이 있습니다", + "optionAlreadyExist": "옵션이 이미 존재합니다" + }, + "rowPage": { + "newField": "새 필드 추가", + "fieldDragElementTooltip": "메뉴 열기", + "showHiddenFields": { + "one": "숨겨진 {count}개의 필드 표시", + "many": "숨겨진 {count}개의 필드 표시", + "other": "숨겨진 {count}개의 필드 표시" + }, + "hideHiddenFields": { + "one": "숨겨진 {count}개의 필드 숨기기", + "many": "숨겨진 {count}개의 필드 숨기기", + "other": "숨겨진 {count}개의 필드 숨기기" + }, + "openAsFullPage": "전체 페이지로 열기", + "moreRowActions": "더 많은 행 작업" }, "sort": { "ascending": "오름차순", "descending": "내림차순", + "by": "기준", + "empty": "활성 정렬 없음", + "cannotFindCreatableField": "정렬할 적절한 필드를 찾을 수 없습니다", + "deleteAllSorts": "모든 정렬 삭제", "addSort": "정렬 추가", - "deleteSort": "정렬 삭제" + "sortsActive": "정렬 중에는 {intention}할 수 없습니다", + "removeSorting": "이 보기의 모든 정렬을 제거하고 계속하시겠습니까?", + "fieldInUse": "이미 이 필드로 정렬 중입니다" }, "row": { + "label": "행", "duplicate": "복제", "delete": "삭제", - "textPlaceholder": "비어있음", - "copyProperty": "속성이 클립보드로 복사됨", + "titlePlaceholder": "제목 없음", + "textPlaceholder": "비어 있음", + "copyProperty": "속성이 클립보드에 복사되었습니다", "count": "개수", - "newRow": "행 추가", - "action": "행동" + "newRow": "새 행", + "loadMore": "더 로드", + "action": "작업", + "add": "아래에 추가하려면 클릭", + "drag": "이동하려면 드래그", + "deleteRowPrompt": "이 행을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "deleteCardPrompt": "이 카드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "dragAndClick": "이동하려면 드래그, 메뉴를 열려면 클릭", + "insertRecordAbove": "위에 레코드 삽입", + "insertRecordBelow": "아래에 레코드 삽입", + "noContent": "내용 없음", + "reorderRowDescription": "행 순서 변경", + "createRowAboveDescription": "위에 행 생성", + "createRowBelowDescription": "아래에 행 삽입" }, "selectOption": { "create": "생성", "purpleColor": "보라색", - "pinkColor": "핑크색", - "lightPinkColor": "연한 핑크색", - "orangeColor": "오렌지색", - "yellowColor": "노랑색", + "pinkColor": "분홍색", + "lightPinkColor": "연분홍색", + "orangeColor": "주황색", + "yellowColor": "노란색", "limeColor": "라임색", - "greenColor": "초록색", - "aquaColor": "아쿠아색", - "blueColor": "파랑색", + "greenColor": "녹색", + "aquaColor": "청록색", + "blueColor": "파란색", "deleteTag": "태그 삭제", "colorPanelTitle": "색상", "panelTitle": "옵션 선택 또는 생성", - "searchOption": "옵션 검색" + "searchOption": "옵션 검색", + "searchOrCreateOption": "옵션 검색 또는 생성", + "createNew": "새로 생성", + "orSelectOne": "또는 옵션 선택", + "typeANewOption": "새 옵션 입력", + "tagName": "태그 이름" }, "checklist": { - "addNew": "항목 추가" + "taskHint": "작업 설명", + "addNew": "새 작업 추가", + "submitNewTask": "생성", + "hideComplete": "완료된 작업 숨기기", + "showComplete": "모든 작업 표시" + }, + "url": { + "launch": "브라우저에서 링크 열기", + "copy": "링크를 클립보드에 복사", + "textFieldHint": "URL 입력", + "copiedNotification": "클립보드에 복사되었습니다!" + }, + "relation": { + "relatedDatabasePlaceLabel": "관련 데이터베이스", + "relatedDatabasePlaceholder": "없음", + "inRelatedDatabase": "에", + "rowSearchTextFieldPlaceholder": "검색", + "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 아래 목록에서 하나를 먼저 선택하세요:", + "emptySearchResult": "레코드를 찾을 수 없습니다", + "linkedRowListLabel": "{count}개의 연결된 행", + "unlinkedRowListLabel": "다른 행 연결" }, "menuName": "그리드", - "referencedGridPrefix": "관점" + "referencedGridPrefix": "보기", + "calculate": "계산", + "calculationTypeLabel": { + "none": "없음", + "average": "평균", + "max": "최대", + "median": "중앙값", + "min": "최소", + "sum": "합계", + "count": "개수", + "countEmpty": "비어 있는 개수", + "countEmptyShort": "비어 있음", + "countNonEmpty": "비어 있지 않은 개수", + "countNonEmptyShort": "채워짐" + }, + "media": { + "rename": "이름 변경", + "download": "다운로드", + "expand": "확장", + "delete": "삭제", + "moreFilesHint": "+{}", + "addFileOrImage": "파일 또는 링크 추가", + "attachmentsHint": "{}", + "addFileMobile": "파일 추가", + "extraCount": "+{}", + "deleteFileDescription": "이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "showFileNames": "파일 이름 표시", + "downloadSuccess": "파일이 다운로드되었습니다", + "downloadFailedToken": "파일을 다운로드하지 못했습니다. 사용자 토큰이 없습니다", + "setAsCover": "표지로 설정", + "openInBrowser": "브라우저에서 열기", + "embedLink": "파일 링크 삽입" + } }, "document": { "menuName": "문서", "date": { - "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwelveHour": "오후 01:00", "timeHintTextInTwentyFourHour": "13:00" }, + "creating": "생성 중...", "slashMenu": { "board": { "selectABoardToLinkTo": "연결할 보드 선택", - "createANewBoard": "새 보드 만들기" + "createANewBoard": "새 보드 생성" }, "grid": { "selectAGridToLinkTo": "연결할 그리드 선택", - "createANewGrid": "새 그리드 만들기" + "createANewGrid": "새 그리드 생성" }, "calendar": { "selectACalendarToLinkTo": "연결할 캘린더 선택", - "createANewCalendar": "새 캘린더 만들기" + "createANewCalendar": "새 캘린더 생성" + }, + "document": { + "selectADocumentToLinkTo": "연결할 문서 선택" + }, + "name": { + "textStyle": "텍스트 스타일", + "list": "목록", + "toggle": "토글", + "fileAndMedia": "파일 및 미디어", + "simpleTable": "간단한 테이블", + "visuals": "시각 자료", + "document": "문서", + "advanced": "고급", + "text": "텍스트", + "heading1": "헤딩 1", + "heading2": "헤딩 2", + "heading3": "헤딩 3", + "image": "이미지", + "bulletedList": "글머리 기호 목록", + "numberedList": "번호 매기기 목록", + "todoList": "할 일 목록", + "doc": "문서", + "linkedDoc": "페이지로 연결", + "grid": "그리드", + "linkedGrid": "연결된 그리드", + "kanban": "칸반", + "linkedKanban": "연결된 칸반", + "calendar": "캘린더", + "linkedCalendar": "연결된 캘린더", + "quote": "인용", + "divider": "구분선", + "table": "테이블", + "callout": "콜아웃", + "outline": "개요", + "mathEquation": "수학 방정식", + "code": "코드", + "toggleList": "토글 목록", + "toggleHeading1": "토글 헤딩 1", + "toggleHeading2": "토글 헤딩 2", + "toggleHeading3": "토글 헤딩 3", + "emoji": "이모지", + "aiWriter": "AI에게 물어보기", + "dateOrReminder": "날짜 또는 알림", + "photoGallery": "사진 갤러리", + "file": "파일", + "twoColumns": "2열", + "threeColumns": "3열", + "fourColumns": "4열" + }, + "subPage": { + "name": "문서", + "keyword1": "하위 페이지", + "keyword2": "페이지", + "keyword3": "자식 페이지", + "keyword4": "페이지 삽입", + "keyword5": "페이지 포함", + "keyword6": "새 페이지", + "keyword7": "페이지 생성", + "keyword8": "문서" } }, "selectionMenu": { - "outline": "개요" + "outline": "개요", + "codeBlock": "코드 블록" }, "plugins": { - "referencedBoard": "참조 보드", + "referencedBoard": "참조된 보드", "referencedGrid": "참조된 그리드", - "referencedCalendar": "참조된 달력", - "autoGeneratorMenuItemName": "AI 작성자", - "autoGeneratorTitleName": "AI: AI에게 무엇이든 쓰라고 요청하세요...", - "autoGeneratorLearnMore": "더 알아보기", - "autoGeneratorGenerate": "생성하다", - "autoGeneratorHintText": "OpenAI에게 물어보세요 ...", - "autoGeneratorCantGetOpenAIKey": "AI 키를 가져올 수 없습니다.", - "autoGeneratorRewrite": "고쳐 쓰기", - "smartEdit": "AI 어시스턴트", + "referencedCalendar": "참조된 캘린더", + "referencedDocument": "참조된 문서", + "aiWriter": { + "userQuestion": "AI에게 물어보기", + "continueWriting": "계속 작성", + "fixSpelling": "맞춤법 및 문법 수정", + "improveWriting": "글쓰기 개선", + "summarize": "요약", + "explain": "설명", + "makeShorter": "짧게 만들기", + "makeLonger": "길게 만들기" + }, + "autoGeneratorMenuItemName": "AI 작성기", + "autoGeneratorTitleName": "AI: AI에게 무엇이든 물어보세요...", + "autoGeneratorLearnMore": "자세히 알아보기", + "autoGeneratorGenerate": "생성", + "autoGeneratorHintText": "AI에게 물어보기 ...", + "autoGeneratorCantGetOpenAIKey": "AI 키를 가져올 수 없습니다", + "autoGeneratorRewrite": "다시 작성", + "smartEdit": "AI에게 물어보기", "aI": "AI", - "smartEditFixSpelling": "맞춤법 수정", + "smartEditFixSpelling": "맞춤법 및 문법 수정", "warning": "⚠️ AI 응답은 부정확하거나 오해의 소지가 있을 수 있습니다.", - "smartEditSummarize": "요약하다", - "smartEditImproveWriting": "쓰기 향상", - "smartEditMakeLonger": "더 길게", - "smartEditCouldNotFetchResult": "OpenAI에서 결과를 가져올 수 없습니다.", - "smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다.", + "smartEditSummarize": "요약", + "smartEditImproveWriting": "글쓰기 개선", + "smartEditMakeLonger": "길게 만들기", + "smartEditCouldNotFetchResult": "AI에서 결과를 가져올 수 없습니다", + "smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다", "smartEditDisabled": "설정에서 AI 연결", - "discardResponse": "AI 응답을 삭제하시겠습니까?", - "createInlineMathEquation": "방정식 만들기", + "appflowyAIEditDisabled": "AI 기능을 활성화하려면 로그인하세요", + "discardResponse": "AI 응답을 버리시겠습니까?", + "createInlineMathEquation": "방정식 생성", + "fonts": "글꼴", + "insertDate": "날짜 삽입", + "emoji": "이모지", "toggleList": "토글 목록", + "emptyToggleHeading": "빈 토글 h{}. 내용을 추가하려면 클릭하세요.", + "emptyToggleList": "빈 토글 목록. 내용을 추가하려면 클릭하세요.", + "emptyToggleHeadingWeb": "빈 토글 h{level}. 내용을 추가하려면 클릭하세요", + "quoteList": "인용 목록", + "numberedList": "번호 매기기 목록", + "bulletedList": "글머리 기호 목록", + "todoList": "할 일 목록", + "callout": "콜아웃", + "simpleTable": { + "moreActions": { + "color": "색상", + "align": "정렬", + "delete": "삭제", + "duplicate": "복제", + "insertLeft": "왼쪽에 삽입", + "insertRight": "오른쪽에 삽입", + "insertAbove": "위에 삽입", + "insertBelow": "아래에 삽입", + "headerColumn": "헤더 열", + "headerRow": "헤더 행", + "clearContents": "내용 지우기", + "setToPageWidth": "페이지 너비로 설정", + "distributeColumnsWidth": "열 너비 균등 분배", + "duplicateRow": "행 복제", + "duplicateColumn": "열 복제", + "textColor": "텍스트 색상", + "cellBackgroundColor": "셀 배경 색상", + "duplicateTable": "테이블 복제" + }, + "clickToAddNewRow": "새 행을 추가하려면 클릭", + "clickToAddNewColumn": "새 열을 추가하려면 클릭", + "clickToAddNewRowAndColumn": "새 행과 열을 추가하려면 클릭", + "headerName": { + "table": "테이블", + "alignText": "텍스트 정렬" + } + }, "cover": { - "changeCover": "커버를 바꾸다", - "colors": "그림 물감", + "changeCover": "표지 변경", + "colors": "색상", "images": "이미지", "clearAll": "모두 지우기", - "abstract": "추상적인", + "abstract": "추상", "addCover": "표지 추가", "addLocalImage": "로컬 이미지 추가", "invalidImageUrl": "잘못된 이미지 URL", - "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다.", + "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다", "enterImageUrl": "이미지 URL 입력", - "add": "추가하다", - "back": "뒤쪽에", + "add": "추가", + "back": "뒤로", "saveToGallery": "갤러리에 저장", "removeIcon": "아이콘 제거", + "removeCover": "표지 제거", "pasteImageUrl": "이미지 URL 붙여넣기", "or": "또는", "pickFromFiles": "파일에서 선택", - "couldNotFetchImage": "이미지를 가져올 수 없습니다.", + "couldNotFetchImage": "이미지를 가져올 수 없습니다", "imageSavingFailed": "이미지 저장 실패", "addIcon": "아이콘 추가", + "changeIcon": "아이콘 변경", "coverRemoveAlert": "삭제 후 표지에서 제거됩니다.", - "alertDialogConfirmation": "너 정말 계속하고 싶니?" + "alertDialogConfirmation": "계속하시겠습니까?" }, "mathEquation": { - "addMathEquation": "수학 방정식 추가", + "name": "수학 방정식", + "addMathEquation": "TeX 방정식 추가", "editMathEquation": "수학 방정식 편집" }, "optionAction": { - "click": "딸깍 하는 소리", + "click": "클릭", "toOpenMenu": " 메뉴 열기", + "drag": "드래그", + "toMove": " 이동", "delete": "삭제", - "duplicate": "복제하다", - "turnInto": "로 변하다", - "moveUp": "이동", + "duplicate": "복제", + "turnInto": "변환", + "moveUp": "위로 이동", "moveDown": "아래로 이동", "color": "색상", - "align": "맞추다", + "align": "정렬", "left": "왼쪽", - "center": "센터", + "center": "가운데", "right": "오른쪽", - "defaultColor": "기본" + "defaultColor": "기본", + "depth": "깊이", + "copyLinkToBlock": "블록 링크 복사" }, "image": { - "copiedToPasteBoard": "이미지 링크가 클립보드에 복사되었습니다." + "addAnImage": "이미지 추가", + "copiedToPasteBoard": "이미지 링크가 클립보드에 복사되었습니다", + "addAnImageDesktop": "이미지 추가", + "addAnImageMobile": "하나 이상의 이미지를 추가하려면 클릭", + "dropImageToInsert": "이미지를 드롭하여 삽입", + "imageUploadFailed": "이미지 업로드 실패", + "imageDownloadFailed": "이미지 다운로드 실패, 다시 시도하세요", + "imageDownloadFailedToken": "사용자 토큰이 없어 이미지 다운로드 실패, 다시 시도하세요", + "errorCode": "오류 코드" + }, + "photoGallery": { + "name": "사진 갤러리", + "imageKeyword": "이미지", + "imageGalleryKeyword": "이미지 갤러리", + "photoKeyword": "사진", + "photoBrowserKeyword": "사진 브라우저", + "galleryKeyword": "갤러리", + "addImageTooltip": "이미지 추가", + "changeLayoutTooltip": "레이아웃 변경", + "browserLayout": "브라우저", + "gridLayout": "그리드", + "deleteBlockTooltip": "전체 갤러리 삭제" + }, + "math": { + "copiedToPasteBoard": "수학 방정식이 클립보드에 복사되었습니다" + }, + "urlPreview": { + "copiedToPasteBoard": "링크가 클립보드에 복사되었습니다", + "convertToLink": "링크로 변환" }, "outline": { - "addHeadingToCreateOutline": "제목을 추가하여 목차를 만듭니다." - } + "addHeadingToCreateOutline": "목차를 만들려면 헤딩을 추가하세요.", + "noMatchHeadings": "일치하는 헤딩이 없습니다." + }, + "table": { + "addAfter": "뒤에 추가", + "addBefore": "앞에 추가", + "delete": "삭제", + "clear": "내용 지우기", + "duplicate": "복제", + "bgColor": "배경 색상" + }, + "contextMenu": { + "copy": "복사", + "cut": "잘라내기", + "paste": "붙여넣기", + "pasteAsPlainText": "서식 없이 붙여넣기" + }, + "action": "작업", + "database": { + "selectDataSource": "데이터 소스 선택", + "noDataSource": "데이터 소스 없음", + "selectADataSource": "데이터 소스 선택", + "toContinue": "계속하려면", + "newDatabase": "새 데이터베이스", + "linkToDatabase": "데이터베이스로 연결" + }, + "date": "날짜", + "video": { + "label": "비디오", + "emptyLabel": "비디오 추가", + "placeholder": "비디오 링크 붙여넣기", + "copiedToPasteBoard": "비디오 링크가 클립보드에 복사되었습니다", + "insertVideo": "비디오 추가", + "invalidVideoUrl": "지원되지 않는 소스 URL입니다.", + "invalidVideoUrlYouTube": "YouTube는 아직 지원되지 않습니다.", + "supportedFormats": "지원되는 형식: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" + }, + "file": { + "name": "파일", + "uploadTab": "업로드", + "uploadMobile": "파일 선택", + "uploadMobileGallery": "사진 갤러리에서", + "networkTab": "링크 삽입", + "placeholderText": "파일 업로드 또는 삽입", + "placeholderDragging": "업로드할 파일을 드롭하세요", + "dropFileToUpload": "업로드할 파일을 드롭하세요", + "fileUploadHint": "파일을 드래그 앤 드롭하거나 클릭하여 ", + "fileUploadHintSuffix": "찾아보기", + "networkHint": "파일 링크 붙여넣기", + "networkUrlInvalid": "잘못된 URL입니다. URL을 확인하고 다시 시도하세요.", + "networkAction": "삽입", + "fileTooBigError": "파일 크기가 너무 큽니다. 10MB 미만의 파일을 업로드하세요", + "renameFile": { + "title": "파일 이름 변경", + "description": "이 파일의 새 이름을 입력하세요", + "nameEmptyError": "파일 이름은 비워둘 수 없습니다." + }, + "uploadedAt": "{}에 업로드됨", + "linkedAt": "{}에 링크 추가됨", + "failedToOpenMsg": "열지 못했습니다. 파일을 찾을 수 없습니다" + }, + "subPage": { + "handlingPasteHint": " - (붙여넣기 처리 중)", + "errors": { + "failedDeletePage": "페이지 삭제 실패", + "failedCreatePage": "페이지 생성 실패", + "failedMovePage": "이 문서로 페이지 이동 실패", + "failedDuplicatePage": "페이지 복제 실패", + "failedDuplicateFindView": "페이지 복제 실패 - 원본 보기를 찾을 수 없습니다" + } + }, + "cannotMoveToItsChildren": "자식으로 이동할 수 없습니다" + }, + "outlineBlock": { + "placeholder": "목차" }, "textBlock": { - "placeholder": "명령에 '/' 입력" + "placeholder": "명령어를 입력하려면 '/'를 입력하세요" }, "title": { - "placeholder": "무제" + "placeholder": "제목 없음" }, "imageBlock": { - "placeholder": "이미지를 추가하려면 클릭하세요.", + "placeholder": "이미지 추가하려면 클릭", "upload": { "label": "업로드", - "placeholder": "이미지를 업로드하려면 클릭하세요." + "placeholder": "이미지 업로드하려면 클릭" }, "url": { "label": "이미지 URL", "placeholder": "이미지 URL 입력" }, + "ai": { + "label": "AI로 이미지 생성", + "placeholder": "AI가 이미지를 생성할 프롬프트를 입력하세요" + }, + "stability_ai": { + "label": "Stability AI로 이미지 생성", + "placeholder": "Stability AI가 이미지를 생성할 프롬프트를 입력하세요" + }, "support": "이미지 크기 제한은 5MB입니다. 지원되는 형식: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "잘못된 이미지", - "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다.", - "invalidImageFormat": "이미지 형식은 지원되지 않습니다. 지원되는 형식: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "잘못된 이미지 URL" + "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다", + "invalidImageFormat": "지원되지 않는 이미지 형식입니다. 지원되는 형식: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "잘못된 이미지 URL", + "noImage": "파일 또는 디렉토리가 없습니다", + "multipleImagesFailed": "하나 이상의 이미지 업로드 실패, 다시 시도하세요" + }, + "embedLink": { + "label": "링크 삽입", + "placeholder": "이미지 링크를 붙여넣거나 입력하세요" + }, + "unsplash": { + "label": "Unsplash" + }, + "searchForAnImage": "이미지 검색", + "pleaseInputYourOpenAIKey": "설정 페이지에서 AI 키를 입력하세요", + "saveImageToGallery": "이미지 저장", + "failedToAddImageToGallery": "이미지 저장 실패", + "successToAddImageToGallery": "이미지가 사진에 저장되었습니다", + "unableToLoadImage": "이미지를 로드할 수 없습니다", + "maximumImageSize": "최대 지원 업로드 이미지 크기는 10MB입니다", + "uploadImageErrorImageSizeTooBig": "이미지 크기는 10MB 미만이어야 합니다", + "imageIsUploading": "이미지 업로드 중", + "openFullScreen": "전체 화면으로 열기", + "interactiveViewer": { + "toolbar": { + "previousImageTooltip": "이전 이미지", + "nextImageTooltip": "다음 이미지", + "zoomOutTooltip": "축소", + "zoomInTooltip": "확대", + "changeZoomLevelTooltip": "확대/축소 수준 변경", + "openLocalImage": "이미지 열기", + "downloadImage": "이미지 다운로드", + "closeViewer": "인터랙티브 뷰어 닫기", + "scalePercentage": "{}%", + "deleteImageTooltip": "이미지 삭제" + } } }, "codeBlock": { "language": { "label": "언어", - "placeholder": "언어 선택" - } + "placeholder": "언어 선택", + "auto": "자동" + }, + "copyTooltip": "복사", + "searchLanguageHint": "언어 검색", + "codeCopiedSnackbar": "코드가 클립보드에 복사되었습니다!" }, "inlineLink": { - "placeholder": "링크 붙여넣기 또는 입력", + "placeholder": "링크를 붙여넣거나 입력하세요", + "openInNewTab": "새 탭에서 열기", + "copyLink": "링크 복사", + "removeLink": "링크 제거", "url": { "label": "링크 URL", "placeholder": "링크 URL 입력" @@ -602,89 +2088,1101 @@ "label": "링크 제목", "placeholder": "링크 제목 입력" } + }, + "mention": { + "placeholder": "사람, 페이지 또는 날짜 언급...", + "page": { + "label": "페이지로 연결", + "tooltip": "페이지 열기" + }, + "deleted": "삭제됨", + "deletedContent": "이 콘텐츠는 존재하지 않거나 삭제되었습니다", + "noAccess": "액세스 불가", + "deletedPage": "삭제된 페이지", + "trashHint": " - 휴지통에 있음", + "morePages": "더 많은 페이지" + }, + "toolbar": { + "resetToDefaultFont": "기본값으로 재설정", + "textSize": "텍스트 크기", + "h1": "헤딩 1", + "h2": "헤딩 2", + "h3": "헤딩 3", + "alignLeft": "왼쪽 정렬", + "alignRight": "오른쪽 정렬", + "alignCenter": "가운데 정렬", + "link": "링크", + "textAlign": "텍스트 정렬", + "moreOptions": "더 많은 옵션", + "font": "글꼴", + "suggestions": "제안", + "turnInto": "변환" + }, + "errorBlock": { + "theBlockIsNotSupported": "블록 콘텐츠를 구문 분석할 수 없습니다", + "clickToCopyTheBlockContent": "블록 콘텐츠를 복사하려면 클릭", + "blockContentHasBeenCopied": "블록 콘텐츠가 복사되었습니다.", + "parseError": "{} 블록을 구문 분석하는 동안 오류가 발생했습니다.", + "copyBlockContent": "블록 콘텐츠 복사" + }, + "mobilePageSelector": { + "title": "페이지 선택", + "failedToLoad": "페이지 목록을 로드하지 못했습니다", + "noPagesFound": "페이지를 찾을 수 없습니다" + }, + "attachmentMenu": { + "choosePhoto": "사진 선택", + "takePicture": "사진 찍기", + "chooseFile": "파일 선택" } }, "board": { "column": { - "createNewCard": "추가" + "label": "열", + "createNewCard": "새로 만들기", + "renameGroupTooltip": "그룹 이름 변경", + "createNewColumn": "새 그룹 추가", + "addToColumnTopTooltip": "맨 위에 새 카드 추가", + "addToColumnBottomTooltip": "맨 아래에 새 카드 추가", + "renameColumn": "이름 변경", + "hideColumn": "숨기기", + "newGroup": "새 그룹", + "deleteColumn": "삭제", + "deleteColumnConfirmation": "이 그룹과 그룹 내 모든 카드를 삭제합니다. 계속하시겠습니까?" }, + "hiddenGroupSection": { + "sectionTitle": "숨겨진 그룹", + "collapseTooltip": "숨겨진 그룹 숨기기", + "expandTooltip": "숨겨진 그룹 보기" + }, + "cardDetail": "카드 세부 정보", + "cardActions": "카드 작업", + "cardDuplicated": "카드가 복제되었습니다", + "cardDeleted": "카드가 삭제되었습니다", + "showOnCard": "카드 세부 정보에 표시", + "setting": "설정", + "propertyName": "속성 이름", "menuName": "보드", - "referencedBoardPrefix": "관점", + "showUngrouped": "그룹화되지 않은 항목 표시", + "ungroupedButtonText": "그룹화되지 않음", + "ungroupedButtonTooltip": "어떤 그룹에도 속하지 않는 카드가 포함되어 있습니다", + "ungroupedItemsTitle": "보드에 추가하려면 클릭", + "groupBy": "그룹 기준", + "groupCondition": "그룹 조건", + "referencedBoardPrefix": "보기", + "notesTooltip": "내부에 노트 있음", "mobile": { + "editURL": "URL 편집", "showGroup": "그룹 표시", "showGroupContent": "이 그룹을 보드에 표시하시겠습니까?", - "failedToLoad": "보드 보기를 로드하지 못했습니다." + "failedToLoad": "보드 보기를 로드하지 못했습니다" + }, + "dateCondition": { + "weekOf": "{} - {} 주", + "today": "오늘", + "yesterday": "어제", + "tomorrow": "내일", + "lastSevenDays": "지난 7일", + "nextSevenDays": "다음 7일", + "lastThirtyDays": "지난 30일", + "nextThirtyDays": "다음 30일" + }, + "noGroup": "그룹화할 속성 없음", + "noGroupDesc": "보드 보기를 표시하려면 그룹화할 속성이 필요합니다", + "media": { + "cardText": "{} {}", + "fallbackName": "파일" } }, "calendar": { - "menuName": "달력", - "defaultNewCalendarTitle": "무제", + "menuName": "캘린더", + "defaultNewCalendarTitle": "제목 없음", + "newEventButtonTooltip": "새 이벤트 추가", "navigation": { "today": "오늘", "jumpToday": "오늘로 이동", - "previousMonth": "지난달", - "nextMonth": "다음 달" + "previousMonth": "이전 달", + "nextMonth": "다음 달", + "views": { + "day": "일", + "week": "주", + "month": "월", + "year": "년" + } + }, + "mobileEventScreen": { + "emptyTitle": "이벤트 없음", + "emptyBody": "이 날에 이벤트를 생성하려면 더하기 버튼을 누르세요." }, "settings": { "showWeekNumbers": "주 번호 표시", - "showWeekends": "주말 보기", - "firstDayOfWeek": "주 시작", - "layoutDateField": "레이아웃 캘린더", + "showWeekends": "주말 표시", + "firstDayOfWeek": "주 시작일", + "layoutDateField": "캘린더 레이아웃 기준", + "changeLayoutDateField": "레이아웃 필드 변경", "noDateTitle": "날짜 없음", - "clickToAdd": "캘린더에 추가하려면 클릭하세요.", - "name": "달력 레이아웃", - "noDateHint": "예약되지 않은 일정이 여기에 표시됩니다." + "noDateHint": { + "zero": "일정이 없는 이벤트가 여기에 표시됩니다", + "one": "{count}개의 일정이 없는 이벤트", + "other": "{count}개의 일정이 없는 이벤트" + }, + "unscheduledEventsTitle": "일정이 없는 이벤트", + "clickToAdd": "캘린더에 추가하려면 클릭", + "name": "캘린더 설정", + "clickToOpen": "레코드를 열려면 클릭" }, - "referencedCalendarPrefix": "관점" + "referencedCalendarPrefix": "보기", + "quickJumpYear": "이동", + "duplicateEvent": "이벤트 복제" }, "errorDialog": { "title": "@:appName 오류", - "howToFixFallback": "불편을 끼쳐드려 죄송합니다! 오류를 설명하는 문제를 GitHub 페이지에 제출하세요.", + "howToFixFallback": "불편을 드려 죄송합니다! GitHub 페이지에 오류를 설명하는 문제를 제출하세요.", + "howToFixFallbackHint1": "불편을 드려 죄송합니다! ", + "howToFixFallbackHint2": " 페이지에 오류를 설명하는 문제를 제출하세요.", "github": "GitHub에서 보기" }, "search": { "label": "검색", + "sidebarSearchIcon": "검색하고 페이지로 빠르게 이동", "placeholder": { - "actions": "검색 작업..." + "actions": "작업 검색..." } }, "message": { "copy": { - "success": "복사했습니다!", + "success": "복사됨!", "fail": "복사할 수 없음" } }, - "unSupportBlock": "현재 버전은 이 블록을 지원하지 않습니다.", + "unSupportBlock": "현재 버전에서는 이 블록을 지원하지 않습니다.", "views": { "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." }, + "colors": { + "custom": "사용자 정의", + "default": "기본", + "red": "빨간색", + "orange": "주황색", + "yellow": "노란색", + "green": "녹색", + "blue": "파란색", + "purple": "보라색", + "pink": "분홍색", + "brown": "갈색", + "gray": "회색" + }, + "emoji": { + "emojiTab": "이모지", + "search": "이모지 검색", + "noRecent": "최근 사용한 이모지 없음", + "noEmojiFound": "이모지를 찾을 수 없음", + "filter": "필터", + "random": "무작위", + "selectSkinTone": "피부 톤 선택", + "remove": "이모지 제거", + "categories": { + "smileys": "스마일리 및 감정", + "people": "사람", + "animals": "자연", + "food": "음식", + "activities": "활동", + "places": "장소", + "objects": "사물", + "symbols": "기호", + "flags": "깃발", + "nature": "자연", + "frequentlyUsed": "자주 사용됨" + }, + "skinTone": { + "default": "기본", + "light": "밝은", + "mediumLight": "중간 밝은", + "medium": "중간", + "mediumDark": "중간 어두운", + "dark": "어두운" + }, + "openSourceIconsFrom": "오픈 소스 아이콘 제공" + }, + "inlineActions": { + "noResults": "결과 없음", + "recentPages": "최근 페이지", + "pageReference": "페이지 참조", + "docReference": "문서 참조", + "boardReference": "보드 참조", + "calReference": "캘린더 참조", + "gridReference": "그리드 참조", + "date": "날짜", + "reminder": { + "groupTitle": "알림", + "shortKeyword": "알림" + }, + "createPage": "\"{}\" 하위 페이지 생성" + }, + "datePicker": { + "dateTimeFormatTooltip": "설정에서 날짜 및 시간 형식 변경", + "dateFormat": "날짜 형식", + "includeTime": "시간 포함", + "isRange": "종료 날짜", + "timeFormat": "시간 형식", + "clearDate": "날짜 지우기", + "reminderLabel": "알림", + "selectReminder": "알림 선택", + "reminderOptions": { + "none": "없음", + "atTimeOfEvent": "이벤트 시간", + "fiveMinsBefore": "5분 전", + "tenMinsBefore": "10분 전", + "fifteenMinsBefore": "15분 전", + "thirtyMinsBefore": "30분 전", + "oneHourBefore": "1시간 전", + "twoHoursBefore": "2시간 전", + "onDayOfEvent": "이벤트 당일", + "oneDayBefore": "1일 전", + "twoDaysBefore": "2일 전", + "oneWeekBefore": "1주일 전", + "custom": "사용자 정의" + } + }, + "relativeDates": { + "yesterday": "어제", + "today": "오늘", + "tomorrow": "내일", + "oneWeek": "1주일" + }, + "notificationHub": { + "title": "알림", + "mobile": { + "title": "업데이트" + }, + "emptyTitle": "모두 확인했습니다!", + "emptyBody": "대기 중인 알림이나 작업이 없습니다. 평온을 즐기세요.", + "tabs": { + "inbox": "받은 편지함", + "upcoming": "다가오는" + }, + "actions": { + "markAllRead": "모두 읽음으로 표시", + "showAll": "모두", + "showUnreads": "읽지 않음" + }, + "filters": { + "ascending": "오름차순", + "descending": "내림차순", + "groupByDate": "날짜별 그룹", + "showUnreadsOnly": "읽지 않은 항목만 표시", + "resetToDefault": "기본값으로 재설정" + } + }, + "reminderNotification": { + "title": "알림", + "message": "잊기 전에 확인하세요!", + "tooltipDelete": "삭제", + "tooltipMarkRead": "읽음으로 표시", + "tooltipMarkUnread": "읽지 않음으로 표시" + }, + "findAndReplace": { + "find": "찾기", + "previousMatch": "이전 일치 항목", + "nextMatch": "다음 일치 항목", + "close": "닫기", + "replace": "교체", + "replaceAll": "모두 교체", + "noResult": "결과 없음", + "caseSensitive": "대소문자 구분", + "searchMore": "더 많은 결과를 찾으려면 검색" + }, + "error": { + "weAreSorry": "죄송합니다", + "loadingViewError": "이 보기를 로드하는 데 문제가 있습니다. 인터넷 연결을 확인하고 앱을 새로 고침하세요. 문제가 계속되면 팀에 문의하세요.", + "syncError": "다른 장치에서 데이터가 동기화되지 않음", + "syncErrorHint": "마지막으로 편집한 장치에서 이 페이지를 다시 열고 현재 장치에서 다시 열어보세요.", + "clickToCopy": "오류 코드를 복사하려면 클릭" + }, + "editor": { + "bold": "굵게", + "bulletedList": "글머리 기호 목록", + "bulletedListShortForm": "글머리 기호", + "checkbox": "체크박스", + "embedCode": "코드 삽입", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "강조", + "color": "색상", + "image": "이미지", + "date": "날짜", + "page": "페이지", + "italic": "기울임꼴", + "link": "링크", + "numberedList": "번호 매기기 목록", + "numberedListShortForm": "번호 매기기", + "toggleHeading1ShortForm": "토글 h1", + "toggleHeading2ShortForm": "토글 h2", + "toggleHeading3ShortForm": "토글 h3", + "quote": "인용", + "strikethrough": "취소선", + "text": "텍스트", + "underline": "밑줄", + "fontColorDefault": "기본", + "fontColorGray": "회색", + "fontColorBrown": "갈색", + "fontColorOrange": "주황색", + "fontColorYellow": "노란색", + "fontColorGreen": "녹색", + "fontColorBlue": "파란색", + "fontColorPurple": "보라색", + "fontColorPink": "분홍색", + "fontColorRed": "빨간색", + "backgroundColorDefault": "기본 배경", + "backgroundColorGray": "회색 배경", + "backgroundColorBrown": "갈색 배경", + "backgroundColorOrange": "주황색 배경", + "backgroundColorYellow": "노란색 배경", + "backgroundColorGreen": "녹색 배경", + "backgroundColorBlue": "파란색 배경", + "backgroundColorPurple": "보라색 배경", + "backgroundColorPink": "분홍색 배경", + "backgroundColorRed": "빨간색 배경", + "backgroundColorLime": "라임색 배경", + "backgroundColorAqua": "청록색 배경", + "done": "완료", + "cancel": "취소", + "tint1": "색조 1", + "tint2": "색조 2", + "tint3": "색조 3", + "tint4": "색조 4", + "tint5": "색조 5", + "tint6": "색조 6", + "tint7": "색조 7", + "tint8": "색조 8", + "tint9": "색조 9", + "lightLightTint1": "보라색", + "lightLightTint2": "분홍색", + "lightLightTint3": "연분홍색", + "lightLightTint4": "주황색", + "lightLightTint5": "노란색", + "lightLightTint6": "라임색", + "lightLightTint7": "녹색", + "lightLightTint8": "청록색", + "lightLightTint9": "파란색", + "urlHint": "URL", + "mobileHeading1": "헤딩 1", + "mobileHeading2": "헤딩 2", + "mobileHeading3": "헤딩 3", + "mobileHeading4": "헤딩 4", + "mobileHeading5": "헤딩 5", + "mobileHeading6": "헤딩 6", + "textColor": "텍스트 색상", + "backgroundColor": "배경 색상", + "addYourLink": "링크 추가", + "openLink": "링크 열기", + "copyLink": "링크 복사", + "removeLink": "링크 제거", + "editLink": "링크 편집", + "linkText": "텍스트", + "linkTextHint": "텍스트를 입력하세요", + "linkAddressHint": "URL을 입력하세요", + "highlightColor": "강조 색상", + "clearHighlightColor": "강조 색상 지우기", + "customColor": "사용자 정의 색상", + "hexValue": "16진수 값", + "opacity": "불투명도", + "resetToDefaultColor": "기본 색상으로 재설정", + "ltr": "LTR", + "rtl": "RTL", + "auto": "자동", + "cut": "잘라내기", + "copy": "복사", + "paste": "붙여넣기", + "find": "찾기", + "select": "선택", + "selectAll": "모두 선택", + "previousMatch": "이전 일치 항목", + "nextMatch": "다음 일치 항목", + "closeFind": "닫기", + "replace": "교체", + "replaceAll": "모두 교체", + "regex": "정규식", + "caseSensitive": "대소문자 구분", + "uploadImage": "이미지 업로드", + "urlImage": "URL 이미지", + "incorrectLink": "잘못된 링크", + "upload": "업로드", + "chooseImage": "이미지 선택", + "loading": "로드 중", + "imageLoadFailed": "이미지 로드 실패", + "divider": "구분선", + "table": "테이블", + "colAddBefore": "앞에 추가", + "rowAddBefore": "앞에 추가", + "colAddAfter": "뒤에 추가", + "rowAddAfter": "뒤에 추가", + "colRemove": "제거", + "rowRemove": "제거", + "colDuplicate": "복제", + "rowDuplicate": "복제", + "colClear": "내용 지우기", + "rowClear": "내용 지우기", + "slashPlaceHolder": "'/'를 입력하여 블록을 삽입하거나 입력 시작", + "typeSomething": "무언가 입력...", + "toggleListShortForm": "토글", + "quoteListShortForm": "인용", + "mathEquationShortForm": "수식", + "codeBlockShortForm": "코드" + }, + "favorite": { + "noFavorite": "즐겨찾기 페이지 없음", + "noFavoriteHintText": "페이지를 왼쪽으로 스와이프하여 즐겨찾기에 추가하세요", + "removeFromSidebar": "사이드바에서 제거", + "addToSidebar": "사이드바에 고정" + }, + "cardDetails": { + "notesPlaceholder": "/를 입력하여 블록을 삽입하거나 입력 시작" + }, + "blockPlaceholders": { + "todoList": "할 일", + "bulletList": "목록", + "numberList": "목록", + "quote": "인용", + "heading": "헤딩 {}" + }, + "titleBar": { + "pageIcon": "페이지 아이콘", + "language": "언어", + "font": "글꼴", + "actions": "작업", + "date": "날짜", + "addField": "필드 추가", + "userIcon": "사용자 아이콘" + }, + "noLogFiles": "로그 파일이 없습니다", "newSettings": { "myAccount": { + "title": "내 계정", + "subtitle": "프로필을 사용자 정의하고, 계정 보안을 관리하고, AI 키를 열거나 계정에 로그인하세요.", + "profileLabel": "계정 이름 및 프로필 이미지", + "profileNamePlaceholder": "이름 입력", + "accountSecurity": "계정 보안", + "2FA": "2단계 인증", + "aiKeys": "AI 키", + "accountLogin": "계정 로그인", + "updateNameError": "이름 업데이트 실패", + "updateIconError": "아이콘 업데이트 실패", + "aboutAppFlowy": "@:appName 정보", "deleteAccount": { - "dialogContent2": "이 작업은 실행 취소할 수 없으며 모든 작업 공간에서의 액세스가 제거되고, 개인 작업 공간을 포함한 전체 계정이 삭제되고, 모든 공유 작업 공간에서 제거됩니다." + "title": "계정 삭제", + "subtitle": "계정과 모든 데이터를 영구적으로 삭제합니다.", + "description": "계정을 영구적으로 삭제하고 모든 작업 공간에서 액세스를 제거합니다.", + "deleteMyAccount": "내 계정 삭제", + "dialogTitle": "계정 삭제", + "dialogContent1": "계정을 영구적으로 삭제하시겠습니까?", + "dialogContent2": "이 작업은 되돌릴 수 없으며, 모든 작업 공간에서 액세스를 제거하고, 개인 작업 공간을 포함한 전체 계정을 삭제하며, 모든 공유 작업 공간에서 제거됩니다.", + "confirmHint1": "\"내 계정 삭제\"를 입력하여 확인하세요.", + "confirmHint2": "이 작업은 되돌릴 수 없으며, 계정과 모든 관련 데이터를 영구적으로 삭제합니다.", + "confirmHint3": "내 계정 삭제", + "checkToConfirmError": "삭제를 확인하려면 확인란을 선택해야 합니다", + "failedToGetCurrentUser": "현재 사용자 이메일을 가져오지 못했습니다", + "confirmTextValidationFailed": "확인 텍스트가 \"내 계정 삭제\"와 일치하지 않습니다", + "deleteAccountSuccess": "계정이 성공적으로 삭제되었습니다" } + }, + "workplace": { + "name": "작업 공간", + "title": "작업 공간 설정", + "subtitle": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.", + "workplaceName": "작업 공간 이름", + "workplaceNamePlaceholder": "작업 공간 이름 입력", + "workplaceIcon": "작업 공간 아이콘", + "workplaceIconSubtitle": "작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다.", + "renameError": "작업 공간 이름 변경 실패", + "updateIconError": "아이콘 업데이트 실패", + "chooseAnIcon": "아이콘 선택", + "appearance": { + "name": "외관", + "themeMode": { + "auto": "자동", + "light": "라이트", + "dark": "다크" + }, + "language": "언어" + } + }, + "syncState": { + "syncing": "동기화 중", + "synced": "동기화됨", + "noNetworkConnected": "네트워크에 연결되지 않음" } }, + "pageStyle": { + "title": "페이지 스타일", + "layout": "레이아웃", + "coverImage": "표지 이미지", + "pageIcon": "페이지 아이콘", + "colors": "색상", + "gradient": "그라데이션", + "backgroundImage": "배경 이미지", + "presets": "프리셋", + "photo": "사진", + "unsplash": "Unsplash", + "pageCover": "페이지 표지", + "none": "없음", + "openSettings": "설정 열기", + "photoPermissionTitle": "@:appName가 사진 라이브러리에 접근하려고 합니다", + "photoPermissionDescription": "@:appName가 문서에 이미지를 추가할 수 있도록 사진에 접근해야 합니다", + "cameraPermissionTitle": "@:appName가 카메라에 접근하려고 합니다", + "cameraPermissionDescription": "카메라에서 문서에 이미지를 추가하려면 @:appName가 카메라에 액세스할 수 있어야 합니다.", + "doNotAllow": "허용하지 않음", + "image": "이미지" + }, + "commandPalette": { + "placeholder": "검색하거나 질문하세요...", + "bestMatches": "최고의 일치 항목", + "recentHistory": "최근 기록", + "navigateHint": "탐색하려면", + "loadingTooltip": "결과를 찾고 있습니다...", + "betaLabel": "베타", + "betaTooltip": "현재 문서의 페이지 및 콘텐츠 검색만 지원합니다", + "fromTrashHint": "휴지통에서", + "noResultsHint": "찾고 있는 항목을 찾지 못했습니다. 다른 용어로 검색해 보세요.", + "clearSearchTooltip": "검색 필드 지우기" + }, "space": { - "createNewSpace": "새로운 스페이스 생성", - "defaultSpaceName": "일반" + "delete": "삭제", + "deleteConfirmation": "삭제: ", + "deleteConfirmationDescription": "이 공간 내의 모든 페이지가 삭제되어 휴지통으로 이동되며, 게시한 모든 페이지가 게시 취소됩니다.", + "rename": "공간 이름 변경", + "changeIcon": "아이콘 변경", + "manage": "공간 관리", + "addNewSpace": "새 공간 생성", + "collapseAllSubPages": "모든 하위 페이지 접기", + "createNewSpace": "새 공간 생성", + "createSpaceDescription": "작업을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.", + "spaceName": "공간 이름", + "spaceNamePlaceholder": "예: 마케팅, 엔지니어링, 인사", + "permission": "공간 권한", + "publicPermission": "공용", + "publicPermissionDescription": "전체 액세스 권한이 있는 모든 작업 공간 멤버", + "privatePermission": "비공개", + "privatePermissionDescription": "이 공간에 접근할 수 있는 사람은 본인뿐입니다", + "spaceIconBackground": "배경 색상", + "spaceIcon": "아이콘", + "dangerZone": "위험 구역", + "unableToDeleteLastSpace": "마지막 공간을 삭제할 수 없습니다", + "unableToDeleteSpaceNotCreatedByYou": "다른 사람이 생성한 공간을 삭제할 수 없습니다", + "enableSpacesForYourWorkspace": "작업 공간에 공간을 활성화하세요", + "title": "공간", + "defaultSpaceName": "일반", + "upgradeSpaceTitle": "공간 활성화", + "upgradeSpaceDescription": "작업 공간을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.", + "upgrade": "업데이트", + "upgradeYourSpace": "여러 공간 생성", + "quicklySwitch": "다음 공간으로 빠르게 전환", + "duplicate": "공간 복제", + "movePageToSpace": "페이지를 공간으로 이동", + "cannotMovePageToDatabase": "페이지를 데이터베이스로 이동할 수 없습니다", + "switchSpace": "공간 전환", + "spaceNameCannotBeEmpty": "공간 이름은 비워둘 수 없습니다", + "success": { + "deleteSpace": "공간이 성공적으로 삭제되었습니다", + "renameSpace": "공간 이름이 성공적으로 변경되었습니다", + "duplicateSpace": "공간이 성공적으로 복제되었습니다", + "updateSpace": "공간이 성공적으로 업데이트되었습니다" + }, + "error": { + "deleteSpace": "공간 삭제 실패", + "renameSpace": "공간 이름 변경 실패", + "duplicateSpace": "공간 복제 실패", + "updateSpace": "공간 업데이트 실패" + }, + "createSpace": "공간 생성", + "manageSpace": "공간 관리", + "renameSpace": "공간 이름 변경", + "mSpaceIconColor": "공간 아이콘 색상", + "mSpaceIcon": "공간 아이콘" }, "publish": { - "saveThisPage": "이 템플릿으로 시작" + "hasNotBeenPublished": "이 페이지는 아직 게시되지 않았습니다", + "spaceHasNotBeenPublished": "아직 공간 게시를 지원하지 않습니다", + "reportPage": "페이지 신고", + "databaseHasNotBeenPublished": "데이터베이스 게시를 아직 지원하지 않습니다.", + "createdWith": "제작", + "downloadApp": "AppFlowy 다운로드", + "copy": { + "codeBlock": "코드 블록의 내용이 클립보드에 복사되었습니다", + "imageBlock": "이미지 링크가 클립보드에 복사되었습니다", + "mathBlock": "수학 방정식이 클립보드에 복사되었습니다", + "fileBlock": "파일 링크가 클립보드에 복사되었습니다" + }, + "containsPublishedPage": "이 페이지에는 하나 이상의 게시된 페이지가 포함되어 있습니다. 계속하면 게시가 취소됩니다. 삭제를 진행하시겠습니까?", + "publishSuccessfully": "성공적으로 게시되었습니다", + "unpublishSuccessfully": "성공적으로 게시 취소되었습니다", + "publishFailed": "게시 실패", + "unpublishFailed": "게시 취소 실패", + "noAccessToVisit": "이 페이지에 접근할 수 없습니다...", + "createWithAppFlowy": "AppFlowy로 웹사이트 만들기", + "fastWithAI": "AI로 빠르고 쉽게.", + "tryItNow": "지금 시도해보세요", + "onlyGridViewCanBePublished": "그리드 보기만 게시할 수 있습니다", + "database": { + "zero": "선택한 {} 보기 게시", + "one": "선택한 {} 보기 게시", + "many": "선택한 {} 보기 게시", + "other": "선택한 {} 보기 게시" + }, + "mustSelectPrimaryDatabase": "기본 보기를 선택해야 합니다", + "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 최소 하나의 데이터베이스를 선택하세요.", + "unableToDeselectPrimaryDatabase": "기본 보기를 선택 해제할 수 없습니다", + "saveThisPage": "이 템플릿으로 시작", + "duplicateTitle": "추가할 위치 선택", + "selectWorkspace": "작업 공간 선택", + "addTo": "추가", + "duplicateSuccessfully": "작업 공간에 추가되었습니다", + "duplicateSuccessfullyDescription": "AppFlowy가 설치되어 있지 않습니까? '다운로드'를 클릭하면 다운로드가 자동으로 시작됩니다.", + "downloadIt": "다운로드", + "openApp": "앱에서 열기", + "duplicateFailed": "복제 실패", + "membersCount": { + "zero": "멤버 없음", + "one": "1명의 멤버", + "many": "{count}명의 멤버", + "other": "{count}명의 멤버" + }, + "useThisTemplate": "템플릿 사용" + }, + "web": { + "continue": "계속", + "or": "또는", + "continueWithGoogle": "Google로 계속", + "continueWithGithub": "GitHub로 계속", + "continueWithDiscord": "Discord로 계속", + "continueWithApple": "Apple로 계속", + "moreOptions": "더 많은 옵션", + "collapse": "접기", + "signInAgreement": "\"계속\"을 클릭하면 AppFlowy의", + "and": "및", + "termOfUse": "이용 약관", + "privacyPolicy": "개인정보 보호정책", + "signInError": "로그인 오류", + "login": "가입 또는 로그인", + "fileBlock": { + "uploadedAt": "{time}에 업로드됨", + "linkedAt": "{time}에 링크 추가됨", + "empty": "파일 업로드 또는 삽입", + "uploadFailed": "업로드 실패, 다시 시도하세요", + "retry": "다시 시도" + }, + "importNotion": "Notion에서 가져오기", + "import": "가져오기", + "importSuccess": "성공적으로 업로드되었습니다", + "importSuccessMessage": "가져오기가 완료되면 알림을 받게 됩니다. 이후 사이드바에서 가져온 페이지를 확인할 수 있습니다.", + "importFailed": "가져오기 실패, 파일 형식을 확인하세요", + "dropNotionFile": "Notion zip 파일을 여기에 드롭하여 업로드하거나 클릭하여 찾아보기", + "error": { + "pageNameIsEmpty": "페이지 이름이 비어 있습니다. 다른 이름을 시도하세요" + } + }, + "globalComment": { + "comments": "댓글", + "addComment": "댓글 추가", + "reactedBy": "반응한 사람", + "addReaction": "반응 추가", + "reactedByMore": "및 {count}명", + "showSeconds": { + "one": "1초 전", + "other": "{count}초 전", + "zero": "방금", + "many": "{count}초 전" + }, + "showMinutes": { + "one": "1분 전", + "other": "{count}분 전", + "many": "{count}분 전" + }, + "showHours": { + "one": "1시간 전", + "other": "{count}시간 전", + "many": "{count}시간 전" + }, + "showDays": { + "one": "1일 전", + "other": "{count}일 전", + "many": "{count}일 전" + }, + "showMonths": { + "one": "1개월 전", + "other": "{count}개월 전", + "many": "{count}개월 전" + }, + "showYears": { + "one": "1년 전", + "other": "{count}년 전", + "many": "{count}년 전" + }, + "reply": "답글", + "deleteComment": "댓글 삭제", + "youAreNotOwner": "이 댓글의 소유자가 아닙니다", + "confirmDeleteDescription": "이 댓글을 삭제하시겠습니까?", + "hasBeenDeleted": "삭제됨", + "replyingTo": "답글 대상", + "noAccessDeleteComment": "이 댓글을 삭제할 수 없습니다", + "collapse": "접기", + "readMore": "더 읽기", + "failedToAddComment": "댓글 추가 실패", + "commentAddedSuccessfully": "댓글이 성공적으로 추가되었습니다.", + "commentAddedSuccessTip": "댓글을 추가하거나 답글을 달았습니다. 최신 댓글을 보려면 상단으로 이동하시겠습니까?" }, "template": { - "deleteFromTemplate": "템플릿 목록에서 제거", - "relatedTemplates": "관련된 템플릿 목록", - "deleteTemplate": "템플릿 제거", - "removeRelatedTemplate": "관련된 템플릿 제거", - "label": "템플릿 목록" + "asTemplate": "템플릿으로 저장", + "name": "템플릿 이름", + "description": "템플릿 설명", + "about": "템플릿 정보", + "deleteFromTemplate": "템플릿에서 삭제", + "preview": "템플릿 미리보기", + "categories": "템플릿 카테고리", + "isNewTemplate": "새 템플릿에 고정", + "featured": "추천에 고정", + "relatedTemplates": "관련 템플릿", + "requiredField": "{field}은(는) 필수 항목입니다", + "addCategory": "\"{category}\" 추가", + "addNewCategory": "새 카테고리 추가", + "addNewCreator": "새 제작자 추가", + "deleteCategory": "카테고리 삭제", + "editCategory": "카테고리 편집", + "editCreator": "제작자 편집", + "category": { + "name": "카테고리 이름", + "icon": "카테고리 아이콘", + "bgColor": "카테고리 배경 색상", + "priority": "카테고리 우선순위", + "desc": "카테고리 설명", + "type": "카테고리 유형", + "icons": "카테고리 아이콘", + "colors": "카테고리 색상", + "byUseCase": "사용 사례별", + "byFeature": "기능별", + "deleteCategory": "카테고리 삭제", + "deleteCategoryDescription": "이 카테고리를 삭제하시겠습니까?", + "typeToSearch": "카테고리 검색..." + }, + "creator": { + "label": "템플릿 제작자", + "name": "제작자 이름", + "avatar": "제작자 아바타", + "accountLinks": "제작자 계정 링크", + "uploadAvatar": "아바타 업로드", + "deleteCreator": "제작자 삭제", + "deleteCreatorDescription": "이 제작자를 삭제하시겠습니까?", + "typeToSearch": "제작자 검색..." + }, + "uploadSuccess": "템플릿이 성공적으로 업로드되었습니다", + "uploadSuccessDescription": "템플릿이 성공적으로 업로드되었습니다. 이제 템플릿 갤러리에서 확인할 수 있습니다.", + "viewTemplate": "템플릿 보기", + "deleteTemplate": "템플릿 삭제", + "deleteSuccess": "템플릿이 성공적으로 삭제되었습니다", + "deleteTemplateDescription": "현재 페이지나 게시 상태에는 영향을 미치지 않습니다. 이 템플릿을 삭제하시겠습니까?", + "addRelatedTemplate": "관련 템플릿 추가", + "removeRelatedTemplate": "관련 템플릿 제거", + "uploadAvatar": "아바타 업로드", + "searchInCategory": "{category}에서 검색", + "label": "템플릿" + }, + "fileDropzone": { + "dropFile": "업로드하려면 파일을 클릭하거나 드래그하세요", + "uploading": "업로드 중...", + "uploadFailed": "업로드 실패", + "uploadSuccess": "업로드 성공", + "uploadSuccessDescription": "파일이 성공적으로 업로드되었습니다", + "uploadFailedDescription": "파일 업로드 실패", + "uploadingDescription": "파일이 업로드 중입니다" + }, + "gallery": { + "preview": "전체 화면으로 열기", + "copy": "복사", + "download": "다운로드", + "prev": "이전", + "next": "다음", + "resetZoom": "확대/축소 재설정", + "zoomIn": "확대", + "zoomOut": "축소" + }, + "invitation": { + "join": "참여", + "on": "에", + "invitedBy": "초대자", + "membersCount": { + "zero": "{count}명의 멤버", + "one": "{count}명의 멤버", + "many": "{count}명의 멤버", + "other": "{count}명의 멤버" + }, + "tip": "아래 연락처 정보로 이 작업 공간에 참여하도록 초대되었습니다. 정보가 잘못된 경우 관리자에게 연락하여 초대를 다시 보내달라고 요청하세요.", + "joinWorkspace": "작업 공간 참여", + "success": "작업 공간에 성공적으로 참여했습니다", + "successMessage": "이제 작업 공간 내의 모든 페이지와 작업 공간에 접근할 수 있습니다.", + "openWorkspace": "AppFlowy 열기", + "alreadyAccepted": "이미 초대를 수락했습니다", + "errorModal": { + "title": "문제가 발생했습니다", + "description": "현재 계정 {email}이(가) 이 작업 공간에 접근할 수 없을 수 있습니다. 올바른 계정으로 로그인하거나 작업 공간 소유자에게 도움을 요청하세요.", + "contactOwner": "소유자에게 연락", + "close": "홈으로 돌아가기", + "changeAccount": "계정 변경" + } + }, + "requestAccess": { + "title": "이 페이지에 접근할 수 없습니다", + "subtitle": "이 페이지의 소유자에게 접근을 요청할 수 있습니다. 승인되면 페이지를 볼 수 있습니다.", + "requestAccess": "접근 요청", + "backToHome": "홈으로 돌아가기", + "tip": "현재 로 로그인 중입니다.", + "mightBe": "다른 계정으로 해야 할 수 있습니다.", + "successful": "요청이 성공적으로 전송되었습니다", + "successfulMessage": "소유자가 요청을 승인하면 알림을 받게 됩니다.", + "requestError": "접근 요청 실패", + "repeatRequestError": "이미 이 페이지에 접근을 요청했습니다" + }, + "approveAccess": { + "title": "작업 공간 참여 요청 승인", + "requestSummary": "이(가) 에 참여하고 에 접근하려고 요청합니다", + "upgrade": "업그레이드", + "downloadApp": "AppFlowy 다운로드", + "approveButton": "승인", + "approveSuccess": "성공적으로 승인되었습니다", + "approveError": "승인 실패, 작업 공간 플랜 한도를 초과하지 않았는지 확인하세요", + "getRequestInfoError": "요청 정보를 가져오지 못했습니다", + "memberCount": { + "zero": "멤버 없음", + "one": "1명의 멤버", + "many": "{count}명의 멤버", + "other": "{count}명의 멤버" + }, + "alreadyProTitle": "작업 공간 플랜 한도에 도달했습니다", + "alreadyProMessage": "에 연락하여 더 많은 멤버를 잠금 해제하도록 요청하세요", + "repeatApproveError": "이미 이 요청을 승인했습니다", + "ensurePlanLimit": "작업 공간 플랜 한도를 초과하지 않았는지 확인하세요. 한도를 초과한 경우 작업 공간 플랜을 하거나 를 고려하세요.", + "requestToJoin": "참여 요청", + "asMember": "멤버로" + }, + "upgradePlanModal": { + "title": "Pro로 업그레이드", + "message": "{name}이(가) 무료 멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 Pro 플랜으로 업그레이드하세요.", + "upgradeSteps": "AppFlowy에서 플랜을 업그레이드하는 방법:", + "step1": "1. 설정으로 이동", + "step2": "2. '플랜' 클릭", + "step3": "3. '플랜 변경' 선택", + "appNote": "참고: ", + "actionButton": "업그레이드", + "downloadLink": "앱 다운로드", + "laterButton": "나중에", + "refreshNote": "성공적으로 업그레이드한 후 새 기능을 활성화하려면 를 클릭하세요.", + "refresh": "여기" + }, + "breadcrumbs": { + "label": "탐색 경로" + }, + "time": { + "justNow": "방금", + "seconds": { + "one": "1초", + "other": "{count}초" + }, + "minutes": { + "one": "1분", + "other": "{count}분" + }, + "hours": { + "one": "1시간", + "other": "{count}시간" + }, + "days": { + "one": "1일", + "other": "{count}일" + }, + "weeks": { + "one": "1주일", + "other": "{count}주일" + }, + "months": { + "one": "1개월", + "other": "{count}개월" + }, + "years": { + "one": "1년", + "other": "{count}년" + }, + "ago": "전", + "yesterday": "어제", + "today": "오늘" + }, + "members": { + "zero": "멤버 없음", + "one": "1명의 멤버", + "many": "{count}명의 멤버", + "other": "{count}명의 멤버" + }, + "tabMenu": { + "close": "닫기", + "closeDisabledHint": "고정된 탭은 닫을 수 없습니다. 먼저 고정을 해제하세요", + "closeOthers": "다른 탭 닫기", + "closeOthersHint": "이 탭을 제외한 모든 고정되지 않은 탭을 닫습니다", + "closeOthersDisabledHint": "모든 탭이 고정되어 있어 닫을 탭을 찾을 수 없습니다", + "favorite": "즐겨찾기", + "unfavorite": "즐겨찾기 해제", + "favoriteDisabledHint": "이 보기를 즐겨찾기에 추가할 수 없습니다", + "pinTab": "고정", + "unpinTab": "고정 해제" + }, + "openFileMessage": { + "success": "파일이 성공적으로 열렸습니다", + "fileNotFound": "파일을 찾을 수 없습니다", + "noAppToOpenFile": "이 파일을 열 수 있는 앱이 없습니다", + "permissionDenied": "이 파일을 열 수 있는 권한이 없습니다", + "unknownError": "파일 열기 실패" + }, + "inviteMember": { + "requestInviteMembers": "작업 공간에 초대", + "inviteFailedMemberLimit": "멤버 한도에 도달했습니다. ", + "upgrade": "업그레이드", + "addEmail": "email@example.com, email2@example.com...", + "requestInvites": "초대 보내기", + "inviteAlready": "이미 이 이메일을 초대했습니다: {email}", + "inviteSuccess": "초대가 성공적으로 전송되었습니다", + "description": "아래에 이메일을 쉼표로 구분하여 입력하세요. 멤버 수에 따라 요금이 부과됩니다.", + "emails": "이메일" + }, + "quickNote": { + "label": "빠른 노트", + "quickNotes": "빠른 노트", + "search": "빠른 노트 검색", + "collapseFullView": "전체 보기 축소", + "expandFullView": "전체 보기 확장", + "createFailed": "빠른 노트 생성 실패", + "quickNotesEmpty": "빠른 노트 없음", + "emptyNote": "빈 노트", + "deleteNotePrompt": "선택한 노트가 영구적으로 삭제됩니다. 삭제하시겠습니까?", + "addNote": "새 노트", + "noAdditionalText": "추가 텍스트 없음" }, "subscribe": { + "upgradePlanTitle": "플랜 비교 및 선택", + "yearly": "연간", + "save": "{discount}% 절약", + "monthly": "월별", + "priceIn": "가격 ", + "free": "무료", + "pro": "Pro", + "freeDescription": "모든 것을 정리하기 위한 최대 2명의 개인용", + "proDescription": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", + "proDuration": { + "monthly": "월별 청구되는 멤버당 월별", + "yearly": "연간 청구되는 멤버당 월별" + }, + "cancel": "다운그레이드", + "changePlan": "Pro 플랜으로 업그레이드", + "everythingInFree": "무료 플랜의 모든 기능 +", + "currentPlan": "현재", + "freeDuration": "영원히", + "freePoints": { + "first": "최대 2명의 협업 작업 공간", + "second": "무제한 페이지 및 블록", + "three": "5 GB 저장 공간", + "four": "지능형 검색", + "five": "20 AI 응답", + "six": "모바일 앱", + "seven": "실시간 협업" + }, + "proPoints": { + "first": "무제한 저장 공간", + "second": "최대 10명의 작업 공간 멤버", + "three": "무제한 AI 응답", + "four": "무제한 파일 업로드", + "five": "맞춤 네임스페이스" + }, "cancelPlan": { + "title": "떠나셔서 아쉽습니다", + "success": "구독이 성공적으로 취소되었습니다", + "description": "@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.", + "commonOther": "기타", + "otherHint": "여기에 답변을 작성하세요", + "questionOne": { + "question": "@:appName Pro 구독을 취소한 이유는 무엇입니까?", + "answerOne": "비용이 너무 높음", + "answerTwo": "기능이 기대에 미치지 못함", + "answerThree": "더 나은 대안을 찾음", + "answerFour": "비용을 정당화할 만큼 충분히 사용하지 않음", + "answerFive": "서비스 문제 또는 기술적 어려움" + }, + "questionTwo": { + "question": "미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?", + "answerOne": "매우 가능성이 높음", + "answerTwo": "어느 정도 가능성이 있음", + "answerThree": "잘 모르겠음", + "answerFour": "가능성이 낮음", + "answerFive": "매우 가능성이 낮음" + }, "questionThree": { - "answerFour": "로컬 AI 모델에 대한 액세스" + "question": "구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?", + "answerOne": "다중 사용자 협업", + "answerTwo": "더 긴 시간 버전 기록", + "answerThree": "무제한 AI 응답", + "answerFour": "로컬 AI 모델 액세스" + }, + "questionFour": { + "question": "@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?", + "answerOne": "훌륭함", + "answerTwo": "좋음", + "answerThree": "보통", + "answerFour": "평균 이하", + "answerFive": "불만족" } } + }, + "ai": { + "contentPolicyViolation": "민감한 콘텐츠로 인해 이미지 생성에 실패했습니다. 입력을 다시 작성하고 다시 시도하세요", + "textLimitReachedDescription": "작업 공간의 무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", + "imageLimitReachedDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", + "limitReachedAction": { + "textDescription": "작업 공간의 무료 AI 응답이 부족합니다. 더 많은 응답을 받으려면 ", + "imageDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. ", + "upgrade": "업그레이드", + "toThe": " ", + "proPlan": "Pro 플랜", + "orPurchaseAn": " 또는 ", + "aiAddon": "AI 애드온을 구매하세요" + }, + "editing": "편집 중", + "analyzing": "분석 중", + "continueWritingEmptyDocumentTitle": "계속 작성 오류", + "continueWritingEmptyDocumentDescription": "문서의 내용을 확장하는 데 문제가 있습니다. 간단한 소개를 작성하면 나머지는 우리가 처리할 수 있습니다!" + }, + "autoUpdate": { + "criticalUpdateTitle": "계속하려면 업데이트가 필요합니다", + "criticalUpdateDescription": "경험을 향상시키기 위해 개선 사항을 추가했습니다! 앱을 계속 사용하려면 {currentVersion}에서 {newVersion}으로 업데이트하세요.", + "criticalUpdateButton": "업데이트", + "bannerUpdateTitle": "새 버전 사용 가능!", + "bannerUpdateDescription": "최신 기능 및 수정 사항을 받으세요. 지금 설치하려면 \"업데이트\"를 클릭하세요", + "bannerUpdateButton": "업데이트", + "settingsUpdateTitle": "새 버전 ({newVersion}) 사용 가능!", + "settingsUpdateDescription": "현재 버전: {currentVersion} (공식 빌드) → {newVersion}", + "settingsUpdateButton": "업데이트", + "settingsUpdateWhatsNew": "새로운 기능" + }, + "lockPage": { + "lockPage": "잠금", + "reLockPage": "다시 잠금", + "lockTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다. 잠금 해제하려면 클릭하세요.", + "pageLockedToast": "페이지가 잠겼습니다. 누군가 잠금을 해제할 때까지 편집이 비활성화됩니다.", + "lockedOperationTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다." + }, + "suggestion": { + "accept": "수락", + "keep": "유지", + "discard": "버리기", + "close": "닫기", + "tryAgain": "다시 시도", + "rewrite": "다시 작성", + "insertBelow": "아래에 삽입" } } From cafdfcca51e6f0fbdb7d4c8d22d274c8761f4e01 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 18 Mar 2025 11:31:18 +0800 Subject: [PATCH 163/384] feat: add download button in file block (#7562) --- .../file/file_block_component.dart | 12 +++++++ .../editor_plugins/file/file_block_menu.dart | 34 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart index afd8188cac..fe50224caa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart @@ -10,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -95,6 +96,17 @@ enum FileUrlType { return 2; } } + + FileUploadTypePB toFileUploadTypePB() { + switch (this) { + case FileUrlType.local: + return FileUploadTypePB.LocalFile; + case FileUrlType.network: + return FileUploadTypePB.NetworkFile; + case FileUrlType.cloud: + return FileUploadTypePB.CloudFile; + } + } } Node fileNode({ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart index d79d5a1994..99529b3b8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart @@ -1,15 +1,17 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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'; import 'package:flutter_bloc/flutter_bloc.dart'; class FileBlockMenu extends StatefulWidget { @@ -59,11 +61,37 @@ class _FileBlockMenuState extends State { final dateFormat = context.read().state.dateFormat; final urlType = FileUrlType.fromIntValue(widget.node.attributes[FileBlockKeys.urlType]); - + final fileUploadType = urlType.toFileUploadTypePB(); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + HoverButton( + itemHeight: 20, + leftIcon: const FlowySvg(FlowySvgs.download_s), + name: LocaleKeys.button_download.tr(), + onTap: () { + final userProfile = widget.editorState.document.root.context + ?.read() + .state + .userProfilePB; + final url = widget.node.attributes[FileBlockKeys.url]; + final name = widget.node.attributes[FileBlockKeys.name]; + if (url != null && name != null) { + final filePB = MediaFilePB( + url: url, + name: name, + uploadType: fileUploadType, + ); + downloadMediaFile( + context, + filePB, + userProfile: userProfile, + ); + } + }, + ), + const VSpace(4), HoverButton( itemHeight: 20, leftIcon: const FlowySvg(FlowySvgs.edit_s), From 69ce105806e51c0b522d7fd300f6060ad5e356fe Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 18 Mar 2025 12:54:38 +0800 Subject: [PATCH 164/384] chore: update translation --- frontend/resources/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index edbe2b07f1..4c485f0934 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -837,7 +837,7 @@ "menuLabel": "AI Settings", "keys": { "enableAISearchTitle": "AI Search", - "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT 4-o, Claude 3,5, Llama 3.1, and Mistral 7B", + "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet, and models available in Ollama", "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", "llmModel": "Language Model", "llmModelType": "Language Model Type", @@ -852,7 +852,7 @@ "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading and may take ßa few minutes, depending on your device", + "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", From 17b355197c4168a8ec6f04ca81bd5fe5cc6a2150 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 18 Mar 2025 15:54:16 +0800 Subject: [PATCH 165/384] chore: update commit --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4ed30fa6f7..6d45957104 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900#19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6a12c1bad70fb9486c7aabf379d72d94cb73a2d5#6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900#19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6a12c1bad70fb9486c7aabf379d72d94cb73a2d5#6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 7ef8620dc9..c687278406 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "19f9ea7f9cc7c811eef3349ac3f1c5e6fce5c900" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" } From ccfbde9a92759cbc03f5f7ed477e97eab7dcc06d Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 18 Mar 2025 16:42:29 +0800 Subject: [PATCH 166/384] chore: update local ai translation --- .../setting_ai_view/local_ai_setting.dart | 65 +++++++++++-------- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++------- frontend/resources/translations/en.json | 3 +- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 19806c52ab..949145afbd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -79,35 +79,46 @@ class LocalAISettingHeader extends StatelessWidget { error: (error) => SizedBox.shrink(), loading: () => const SizedBox.shrink(), isEnabled: (isEnabled) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + Row( + children: [ + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + ), + const Spacer(), + Toggle( + value: isEnabled, + onChanged: (_) { + if (isEnabled) { + showConfirmDialog( + context: context, + title: LocaleKeys + .settings_aiPage_keys_disableLocalAITitle + .tr(), + description: LocaleKeys + .settings_aiPage_keys_disableLocalAIDescription + .tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () => context + .read() + .add(const LocalAIToggleEvent.toggle()), + ); + } else { + context + .read() + .add(const LocalAIToggleEvent.toggle()); + } + }, + ), + ], ), - const Spacer(), - Toggle( - value: isEnabled, - onChanged: (_) { - if (isEnabled) { - showConfirmDialog( - context: context, - title: LocaleKeys - .settings_aiPage_keys_disableLocalAITitle - .tr(), - description: LocaleKeys - .settings_aiPage_keys_disableLocalAIDescription - .tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () => context - .read() - .add(const LocalAIToggleEvent.toggle()), - ); - } else { - context - .read() - .add(const LocalAIToggleEvent.toggle()); - } - }, + const VSpace(4), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), + maxLines: 3, + fontSize: 12, ), ], ); diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4c485f0934..ff09cb537e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -858,7 +858,8 @@ "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", "disableLocalAIDescription": "Do you want to disable local AI?", - "localAIToggleTitle": "Toggle to enable or disable local AI", + "localAIToggleTitle": "AppFlowy Local AI (LAI)", + "localAIToggleSubTitle": "Run the most advanced local AI models within AppFlowy for ultimate privacy and security", "offlineAIInstruction1": "Follow the", "offlineAIInstruction2": "instruction", "offlineAIInstruction3": "to enable offline AI.", From 22b03eee2902a5e0d9b6ca53c3dd84c0c792c64f Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:14:20 +0800 Subject: [PATCH 167/384] chore: implement ai writer history (#7523) * chore: implement ai writer history * chore: pass hitosyr --- .../lib/ai/service/appflowy_ai_service.dart | 7 ++++ .../ai/operations/ai_writer_cubit.dart | 35 +++++++++++++++++ .../ai/operations/ai_writer_entities.dart | 38 +++++++++++++++++++ .../base/markdown_text_robot.dart | 2 + .../ai_writer_test/ai_writer_bloc_test.dart | 4 ++ .../event-integration-test/src/chat_event.rs | 1 + frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 6 +-- frontend/rust-lib/flowy-ai/src/completion.rs | 6 ++- frontend/rust-lib/flowy-ai/src/entities.rs | 29 +++++++++++++- 9 files changed, 121 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 2709b1d59c..3c1d5c5a9d 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -3,6 +3,7 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -19,6 +20,8 @@ abstract class AIRepository { String? objectId, required String text, PredefinedFormat? format, + List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, @@ -34,6 +37,7 @@ class AppFlowyAIService implements AIRepository { required String text, PredefinedFormat? format, List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, @@ -47,6 +51,8 @@ class AppFlowyAIService implements AIRepository { onError: onError, ); + final records = history.map((record) => record.toPB()).toList(); + final payload = CompleteTextPB( text: text, completionType: completionType, @@ -57,6 +63,7 @@ class AppFlowyAIService implements AIRepository { if (objectId != null) objectId, ...sourceIds, ].unique(), + history: records, ); return AIEventCompleteText(payload).send().fold( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index d31dc45dcd..2fb00c0b58 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -44,6 +44,7 @@ class AiWriterCubit extends Cubit { final AppFlowyAIService _aiService; final MarkdownTextRobot _textRobot; + final List records = []; final ValueNotifier> selectedSourcesNotifier; (String, PredefinedFormat?)? _previousPrompt; bool acceptReplacesOriginal = false; @@ -66,6 +67,7 @@ class AiWriterCubit extends Cubit { ) async { final command = AiWriterCommand.userQuestion; final node = getAiWriterNode(); + _previousPrompt = (prompt, format); final stream = await _aiService.streamCompletion( @@ -74,6 +76,7 @@ class AiWriterCubit extends Cubit { format: format, sourceIds: selectedSourcesNotifier.value, completionType: command.toCompletionType(), + history: records, onStart: () async { final transaction = editorState.transaction; final position = @@ -87,6 +90,9 @@ class AiWriterCubit extends Cubit { ), ); _textRobot.start(position: position); + records.add( + AiWriterRecord.user(content: prompt), + ); }, onProcess: (text) async { await _textRobot.appendMarkdownText( @@ -99,9 +105,15 @@ class AiWriterCubit extends Cubit { attributes: ApplySuggestionFormatType.replace.attributes, ); emit(ReadyAiWriterState(command, isFirstRun: false)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); }, onError: (error) async { emit(ErrorAiWriterState(state.command, error: error)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); }, ); @@ -337,6 +349,7 @@ class AiWriterCubit extends Cubit { objectId: documentId, text: text, completionType: command.toCompletionType(), + history: records, onStart: () async { final transaction = editorState.transaction; final position = @@ -364,9 +377,15 @@ class AiWriterCubit extends Cubit { ); emit(ReadyAiWriterState(command, isFirstRun: false)); } + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); }, onError: (error) async { emit(ErrorAiWriterState(command, error: error)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); }, ); if (stream != null) { @@ -392,6 +411,7 @@ class AiWriterCubit extends Cubit { objectId: documentId, text: await editorState.getMarkdownInSelection(selection), completionType: command.toCompletionType(), + history: records, onStart: () async { final transaction = editorState.transaction; formatSelection( @@ -429,10 +449,16 @@ class AiWriterCubit extends Cubit { isFirstRun: false, ), ); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); } }, onError: (error) async { emit(ErrorAiWriterState(command, error: error)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); }, ); if (stream != null) { @@ -456,6 +482,7 @@ class AiWriterCubit extends Cubit { objectId: documentId, text: await editorState.getMarkdownInSelection(selection), completionType: command.toCompletionType(), + history: records, onStart: () async {}, onProcess: (text) async { if (state case final GeneratingAiWriterState generatingState) { @@ -477,9 +504,17 @@ class AiWriterCubit extends Cubit { markdownText: generatingState.markdownText, ), ); + records.add( + AiWriterRecord.ai(content: generatingState.markdownText), + ); } }, onError: (error) async { + if (state case final GeneratingAiWriterState generatingState) { + records.add( + AiWriterRecord.ai(content: generatingState.markdownText), + ); + } emit(ErrorAiWriterState(command, error: error)); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart index 5c1909564e..7dc8ffc04e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '../ai_writer_block_component.dart'; @@ -120,3 +121,40 @@ enum ApplySuggestionFormatType { Map get attributes => {AiWriterBlockKeys.suggestion: value}; } + +enum AiRole { + user, + system, + ai, +} + +class AiWriterRecord extends Equatable { + const AiWriterRecord({ + required this.role, + required this.content, + }); + + const AiWriterRecord.user({ + required this.content, + }) : role = AiRole.user; + + const AiWriterRecord.ai({ + required this.content, + }) : role = AiRole.ai; + + final AiRole role; + final String content; + + @override + List get props => [role, content]; + + CompletionRecordPB toPB() { + return CompletionRecordPB( + content: content, + role: switch (role) { + AiRole.user => ChatMessageTypePB.User, + AiRole.system || AiRole.ai => ChatMessageTypePB.System, + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index e818e9437b..58c4deb1b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -30,6 +30,8 @@ class MarkdownTextRobot { bool get hasAnyResult => _markdownText.isNotEmpty; + String get markdownText => _markdownText; + Selection? getInsertedSelection() { final position = _insertPosition; if (position == null) { diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index 73ad2736a0..aab5de8169 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -23,6 +23,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, @@ -53,6 +54,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, @@ -79,6 +81,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, @@ -107,6 +110,7 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], + List history = const [], required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) onProcess, diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index c5ac604397..f12f3d200a 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -100,6 +100,7 @@ impl EventIntegrationTest { object_id: "".to_string(), rag_ids: vec![], format: None, + history: vec![], }; EventBuilder::new(self.clone()) .event(AIEvent::CompleteText) diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index f8fdeb212d..2043c123c0 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,8 +1,8 @@ use bytes::Bytes; pub use client_api::entity::ai_dto::{ - AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionType, CreateChatContext, - LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, RelatedQuestion, - RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, + AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionRecord, CompletionType, + CreateChatContext, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, + RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 7c5b77e030..4ed073f0ca 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -98,6 +98,8 @@ impl CompletionTask { }; let _ = sink.send("start:".to_string()).await; + let completion_history = Some(self.context.history.iter().map(Into::into).collect()); + let format = self.context.format.map(Into::into).unwrap_or_default(); let params = CompleteTextParams { text: self.context.text, completion_type: Some(complete_type), @@ -106,9 +108,9 @@ impl CompletionTask { object_id: self.context.object_id, workspace_id: Some(self.workspace_id.clone()), rag_ids: Some(self.context.rag_ids), - completion_history: None, + completion_history, }), - format: self.context.format.map(Into::into).unwrap_or_default(), + format, }; info!("start completion: {:?}", params); diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 9ffa5f3256..4b5e91db11 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -4,8 +4,8 @@ use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; use flowy_ai_pub::cloud::{ - ChatMessage, ChatMessageMetadata, ChatMessageType, LLMModel, OutputContent, OutputLayout, - RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, + ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionRecord, LLMModel, OutputContent, + OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -358,6 +358,9 @@ pub struct CompleteTextPB { #[pb(index = 6)] pub rag_ids: Vec, + + #[pb(index = 7)] + pub history: Vec, } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -378,6 +381,28 @@ pub enum CompletionTypePB { MakeLonger = 6, } +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct CompletionRecordPB { + #[pb(index = 1)] + pub role: ChatMessageTypePB, + + #[pb(index = 2)] + pub content: String, +} + +impl From<&CompletionRecordPB> for CompletionRecord { + fn from(value: &CompletionRecordPB) -> Self { + CompletionRecord { + role: match value.role { + // Coerce ChatMessageTypePB::System to AI + ChatMessageTypePB::System => "ai".to_string(), + ChatMessageTypePB::User => "human".to_string(), + }, + content: value.content.clone(), + } + } +} + #[derive(Default, ProtoBuf, Clone, Debug)] pub struct ChatStatePB { #[pb(index = 1)] From a89dd87c162e0fe3962d3a60e242a092bf2f8916 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 18 Mar 2025 17:53:21 +0800 Subject: [PATCH 168/384] fix: optimize cover title position offset calculation (#7568) * fix: optimize cover title position offset calculation * feat: ingore shift+enter in callout/quote and fallback to system behavior --- .../callout/callout_block_shortcuts.dart | 3 +- .../header/document_cover_widget.dart | 79 +++++++++++-------- .../quote/quote_block_shortcuts.dart | 4 +- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart index 482364bbb5..842f3f59fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart @@ -32,7 +32,8 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { await editorState.deleteSelection(selection); if (HardwareKeyboard.instance.isShiftPressed) { - await editorState.insertNewLine(); + // ignore the shift+enter event, fallback to the default behavior + return false; } else if (node.children.isEmpty) { // insert a new paragraph within the callout block final path = node.path.child(0); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 7cfd28395a..16605367ca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -188,52 +188,65 @@ class _DocumentCoverWidgetState extends State { onChangeCover: (type, details) => _saveIconOrCover(cover: (type, details)), ), - _buildCoverIcon( - context, - constraints, - offset, - ), + _buildAlignedCoverIcon(context), ], ), - Padding( - padding: EdgeInsets.fromLTRB(offset, 0, offset, 12), - child: Visibility( - visible: offset != 0, - child: MouseRegion( - onEnter: (event) => isCoverTitleHovered.value = true, - onExit: (event) => isCoverTitleHovered.value = false, - child: CoverTitle( - view: widget.view, - ), - ), - ), - ), + _buildAlignedTitle(context), ], ); }, ); } - Widget _buildCoverIcon( - BuildContext context, - BoxConstraints constraints, - double offset, - ) { - if (!hasIcon || offset == 0) { + Widget _buildAlignedTitle(BuildContext context) { + return Center( + child: Container( + constraints: BoxConstraints( + maxWidth: widget.editorState.editorStyle.maxWidth ?? double.infinity, + ), + padding: widget.editorState.editorStyle.padding + + const EdgeInsets.symmetric(horizontal: 44), + child: MouseRegion( + onEnter: (event) => isCoverTitleHovered.value = true, + onExit: (event) => isCoverTitleHovered.value = false, + child: CoverTitle( + view: widget.view, + ), + ), + ), + ); + } + + Widget _buildAlignedCoverIcon(BuildContext context) { + if (!hasIcon) { return const SizedBox.shrink(); } return Positioned( - // if hasCover, there shouldn't be icons present so the icon can - // be closer to the bottom. - left: offset, bottom: hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight, - child: DocumentIcon( - editorState: widget.editorState, - node: widget.node, - icon: viewIcon, - documentId: view.id, - onChangeIcon: (icon) => _saveIconOrCover(icon: icon), + left: 0, + right: 0, + child: Center( + child: Container( + constraints: BoxConstraints( + maxWidth: + widget.editorState.editorStyle.maxWidth ?? double.infinity, + ), + padding: widget.editorState.editorStyle.padding + + const EdgeInsets.symmetric(horizontal: 44), + child: Row( + children: [ + DocumentIcon( + editorState: widget.editorState, + node: widget.node, + icon: viewIcon, + documentId: view.id, + onChangeIcon: (icon) => _saveIconOrCover(icon: icon), + ), + Spacer(), + ], + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart index 914aa7163c..47c6549923 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart @@ -30,8 +30,8 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { await editorState.deleteSelection(selection); if (HardwareKeyboard.instance.isShiftPressed) { - await editorState.insertNewLine(); - return true; + // ignore the shift+enter event, fallback to the default behavior + return false; } else if (node.children.isEmpty && selection.endIndex == node.delta?.length) { // insert a new paragraph within the callout block From 69571f668c6ed295d15dcb9542e828ba9f49979e Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 18 Mar 2025 18:45:31 +0800 Subject: [PATCH 169/384] fix: some launch review issues (#7566) * chore: adjust some toolbar text * chore: change the icons in turn-into menu * chore: change the icon in color menu * fix: keep selection after doing some changes from toolbar * fix: color menu displaying error * fix: wrong filter logic in toolbar suggestion menu * fix: some launch review issues * fix: test errors --- .../document/document_option_action_test.dart | 8 +- ...cument_with_inline_math_equation_test.dart | 5 - .../document/presentation/editor_page.dart | 6 +- .../actions/block_action_option_cubit.dart | 3 + .../option/turn_into_option_action.dart | 40 +-- .../ai/ai_writer_toolbar_item.dart | 2 +- .../desktop_toolbar/color_picker.dart | 330 ++++++++++++++++++ .../desktop_floating_toolbar.dart | 10 +- .../custom_format_toolbar_items.dart | 50 ++- .../custom_hightlight_color_toolbar_item.dart | 12 +- .../custom_placeholder_toolbar_item.dart | 2 +- .../custom_text_align_toolbar_item.dart | 2 +- .../custom_text_color_toolbar_item.dart | 16 +- .../more_option_toolbar_item.dart | 36 +- .../text_heading_toolbar_item.dart | 7 +- .../text_suggestions_toolbar_item.dart | 9 +- frontend/resources/translations/en.json | 17 +- 17 files changed, 475 insertions(+), 80 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index d19dee8b6d..6ec12287a8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -76,9 +76,9 @@ void main() { LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_bulletedList.tr(): + LocaleKeys.editor_bulletedListShortForm.tr(): BulletedListBlockKeys.type, - LocaleKeys.document_slashMenu_name_numberedList.tr(): + LocaleKeys.editor_numberedListShortForm.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, @@ -116,9 +116,9 @@ void main() { LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_bulletedList.tr(): + LocaleKeys.editor_bulletedListShortForm.tr(): BulletedListBlockKeys.type, - LocaleKeys.document_slashMenu_name_numberedList.tr(): + LocaleKeys.editor_numberedListShortForm.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index 523319abf5..67e0149cd1 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -106,11 +106,6 @@ void main() { ); await tester.tapButton(moreOptionButton); - // expect to the see the inline math equation button is highlighted - expect( - find.byFlowySvg(FlowySvgs.toolbar_check_m), - findsOneWidget, - ); // cancel the format await tester.tapButton(inlineMathEquationButton); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 289b299de1..09685aef5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -22,7 +22,6 @@ import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -440,8 +439,9 @@ class _AppFlowyEditorPageState extends State toolbarElevation: 10, ), items: toolbarItems, - decoration: context.getPopoverDecoration( - borderRadius: BorderRadius.circular(6), + decoration: ShapeDecoration( + color: Theme.of(context).cardColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), toolbarBuilder: (context, child) => DesktopFloatingToolbar( editorState: editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index 8c2f99cdd5..35e92d7170 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -265,6 +265,7 @@ class BlockActionOptionCubit extends Cubit { EditorState editorState, { int? level, String? currentViewId, + bool keepSelection = false, }) async { final selection = editorState.selection; if (selection == null) { @@ -289,6 +290,7 @@ class BlockActionOptionCubit extends Cubit { selectedNodes: selectedNodes, level: level, editorState: editorState, + afterSelection: keepSelection ? selection : null, )) { return true; } @@ -347,6 +349,7 @@ class BlockActionOptionCubit extends Cubit { insertedNode, ); transaction.deleteNodes(selectedNodes); + if (keepSelection) transaction.afterSelection = selection; await editorState.apply(transaction); return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart index aa519b7265..ffb238303e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart @@ -274,40 +274,40 @@ class _TurnInfoButton extends StatelessWidget { FlowySvgData _buildLeftIcon(String type, {int? level}) { if (type == ParagraphBlockKeys.type) { - return FlowySvgs.slash_menu_icon_text_s; + return FlowySvgs.type_text_m; } else if (type == HeadingBlockKeys.type) { switch (level) { case 1: - return FlowySvgs.slash_menu_icon_h1_s; + return FlowySvgs.type_h1_m; case 2: - return FlowySvgs.slash_menu_icon_h2_s; + return FlowySvgs.type_h2_m; case 3: - return FlowySvgs.slash_menu_icon_h3_s; + return FlowySvgs.type_h3_m; default: - return FlowySvgs.slash_menu_icon_text_s; + return FlowySvgs.type_text_m; } } else if (type == QuoteBlockKeys.type) { - return FlowySvgs.slash_menu_icon_quote_s; + return FlowySvgs.type_quote_m; } else if (type == BulletedListBlockKeys.type) { - return FlowySvgs.slash_menu_icon_bulleted_list_s; + return FlowySvgs.type_bulleted_list_m; } else if (type == NumberedListBlockKeys.type) { - return FlowySvgs.slash_menu_icon_numbered_list_s; + return FlowySvgs.type_numbered_list_m; } else if (type == TodoListBlockKeys.type) { - return FlowySvgs.slash_menu_icon_checkbox_s; + return FlowySvgs.type_todo_m; } else if (type == CalloutBlockKeys.type) { - return FlowySvgs.slash_menu_icon_callout_s; + return FlowySvgs.type_callout_m; } else if (type == SubPageBlockKeys.type) { return FlowySvgs.icon_document_s; } else if (type == ToggleListBlockKeys.type) { switch (level) { case 1: - return FlowySvgs.toggle_heading1_s; + return FlowySvgs.type_toggle_h1_m; case 2: - return FlowySvgs.toggle_heading2_s; + return FlowySvgs.type_toggle_h2_m; case 3: - return FlowySvgs.toggle_heading3_s; + return FlowySvgs.type_toggle_h3_m; default: - return FlowySvgs.slash_menu_icon_toggle_s; + return FlowySvgs.type_toggle_list_m; } } @@ -335,9 +335,9 @@ class _TurnInfoButton extends StatelessWidget { case QuoteBlockKeys.type: return LocaleKeys.document_slashMenu_name_quote.tr(); case BulletedListBlockKeys.type: - return LocaleKeys.document_slashMenu_name_bulletedList.tr(); + return LocaleKeys.editor_bulletedListShortForm.tr(); case NumberedListBlockKeys.type: - return LocaleKeys.document_slashMenu_name_numberedList.tr(); + return LocaleKeys.editor_numberedListShortForm.tr(); case TodoListBlockKeys.type: return LocaleKeys.editor_checkbox.tr(); case CalloutBlockKeys.type: @@ -347,13 +347,13 @@ class _TurnInfoButton extends StatelessWidget { case ToggleListBlockKeys.type: switch (level) { case 1: - return LocaleKeys.document_slashMenu_name_toggleHeading1.tr(); + return LocaleKeys.editor_toggleHeading1ShortForm.tr(); case 2: - return LocaleKeys.document_slashMenu_name_toggleHeading2.tr(); + return LocaleKeys.editor_toggleHeading2ShortForm.tr(); case 3: - return LocaleKeys.document_slashMenu_name_toggleHeading3.tr(); + return LocaleKeys.editor_toggleHeading3ShortForm.tr(); default: - return LocaleKeys.document_slashMenu_name_toggleList.tr(); + return LocaleKeys.editor_toggleListShortForm.tr(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 1e17a0b321..dc20f2363f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -61,7 +61,7 @@ class _AiWriterToolbarActionListState extends State { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(-8.0, 2.0), + offset: const Offset(0, 2.0), onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart new file mode 100644 index 0000000000..162c7a1c34 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart @@ -0,0 +1,330 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/overlay_util.dart'; +import 'package:flutter/material.dart'; + +class ColorPicker extends StatefulWidget { + const ColorPicker({ + super.key, + required this.title, + required this.selectedColorHex, + required this.onSubmittedColorHex, + required this.colorOptions, + this.resetText, + this.customColorHex, + this.resetIconName, + this.showClearButton = false, + }); + + final String title; + final String? selectedColorHex; + final String? customColorHex; + final void Function(String? color, bool isCustomColor) onSubmittedColorHex; + final String? resetText; + final String? resetIconName; + final bool showClearButton; + + final List colorOptions; + + @override + State createState() => _ColorPickerState(); +} + +class _ColorPickerState extends State { + final TextEditingController _colorHexController = TextEditingController(); + final TextEditingController _colorOpacityController = TextEditingController(); + + @override + void initState() { + super.initState(); + final selectedColorHex = widget.selectedColorHex, + customColorHex = widget.customColorHex; + _colorHexController.text = + _extractColorHex(customColorHex ?? selectedColorHex) ?? 'FFFFFF'; + _colorOpacityController.text = + _convertHexToOpacity(customColorHex ?? selectedColorHex) ?? '100'; + } + + @override + Widget build(BuildContext context) { + return basicOverlay( + context, + width: 300, + height: 250, + children: [ + EditorOverlayTitle(text: widget.title), + const SizedBox(height: 6), + widget.showClearButton && + widget.resetText != null && + widget.resetIconName != null + ? ResetColorButton( + resetText: widget.resetText!, + resetIconName: widget.resetIconName!, + onPressed: (color) => + widget.onSubmittedColorHex.call(color, false), + ) + : const SizedBox.shrink(), + CustomColorItem( + colorController: _colorHexController, + opacityController: _colorOpacityController, + onSubmittedColorHex: (color) => + widget.onSubmittedColorHex.call(color, true), + ), + _buildColorItems( + widget.colorOptions, + widget.selectedColorHex, + ), + ], + ); + } + + Widget _buildColorItems( + List options, + String? selectedColor, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: options + .map((e) => _buildColorItem(e, e.colorHex == selectedColor)) + .toList(), + ); + } + + Widget _buildColorItem(ColorOption option, bool isChecked) { + return SizedBox( + height: 36, + child: TextButton.icon( + onPressed: () { + widget.onSubmittedColorHex(option.colorHex, false); + }, + icon: SizedBox.square( + dimension: 12, + child: Container( + decoration: BoxDecoration( + color: option.colorHex.tryToColor(), + shape: BoxShape.circle, + ), + ), + ), + style: buildOverlayButtonStyle(context), + label: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + option.name, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + style: TextStyle( + color: Theme.of(context).textTheme.labelLarge?.color, + ), + ), + ), + // checkbox + if (isChecked) const FlowySvg(FlowySvgs.toolbar_check_m), + ], + ), + ), + ); + } + + String? _convertHexToOpacity(String? colorHex) { + if (colorHex == null) return null; + final opacityHex = colorHex.substring(2, 4); + final opacity = int.parse(opacityHex, radix: 16) / 2.55; + return opacity.toStringAsFixed(0); + } + + String? _extractColorHex(String? colorHex) { + if (colorHex == null) return null; + return colorHex.substring(4); + } +} + +class ResetColorButton extends StatelessWidget { + const ResetColorButton({ + super.key, + required this.resetText, + required this.resetIconName, + required this.onPressed, + }); + + final Function(String? color) onPressed; + final String resetText; + final String resetIconName; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 32, + child: TextButton.icon( + onPressed: () => onPressed(null), + icon: EditorSvg( + name: resetIconName, + width: 13, + height: 13, + color: Theme.of(context).iconTheme.color, + ), + label: Text( + resetText, + style: TextStyle( + color: Theme.of(context).hintColor, + ), + textAlign: TextAlign.left, + ), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.hovered)) { + return Theme.of(context).hoverColor; + } + return Colors.transparent; + }, + ), + alignment: Alignment.centerLeft, + ), + ), + ); + } +} + +class CustomColorItem extends StatefulWidget { + const CustomColorItem({ + super.key, + required this.colorController, + required this.opacityController, + required this.onSubmittedColorHex, + }); + + final TextEditingController colorController; + final TextEditingController opacityController; + final void Function(String color) onSubmittedColorHex; + + @override + State createState() => _CustomColorItemState(); +} + +class _CustomColorItemState extends State { + @override + Widget build(BuildContext context) { + return ExpansionTile( + tilePadding: const EdgeInsets.only(left: 8), + shape: Border.all( + color: Colors.transparent, + ), // remove the default border when it is expanded + title: Row( + children: [ + // color sample box + SizedBox.square( + dimension: 12, + child: Container( + decoration: BoxDecoration( + color: Color( + int.tryParse( + _combineColorHexAndOpacity( + widget.colorController.text, + widget.opacityController.text, + ), + ) ?? + 0xFFFFFFFF, + ), + shape: BoxShape.circle, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + AppFlowyEditorL10n.current.customColor, + style: Theme.of(context).textTheme.labelLarge, + // same style as TextButton.icon + ), + ), + ], + ), + children: [ + const SizedBox(height: 6), + _customColorDetailsTextField( + labelText: AppFlowyEditorL10n.current.hexValue, + controller: widget.colorController, + // update the color sample box when the text changes + onChanged: (_) => setState(() {}), + onSubmitted: _submitCustomColorHex, + ), + const SizedBox(height: 10), + _customColorDetailsTextField( + labelText: AppFlowyEditorL10n.current.opacity, + controller: widget.opacityController, + // update the color sample box when the text changes + onChanged: (_) => setState(() {}), + onSubmitted: _submitCustomColorHex, + ), + const SizedBox(height: 6), + ], + ); + } + + Widget _customColorDetailsTextField({ + required String labelText, + required TextEditingController controller, + Function(String)? onChanged, + Function(String)? onSubmitted, + }) { + return Padding( + padding: const EdgeInsets.only(right: 3), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: labelText, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + style: Theme.of(context).textTheme.bodyMedium, + onChanged: onChanged, + onSubmitted: onSubmitted, + ), + ); + } + + String _combineColorHexAndOpacity(String colorHex, String opacity) { + colorHex = _fixColorHex(colorHex); + opacity = _fixOpacity(opacity); + final opacityHex = (int.parse(opacity) * 2.55).round().toRadixString(16); + return '0x$opacityHex$colorHex'; + } + + String _fixColorHex(String colorHex) { + if (colorHex.length > 6) { + colorHex = colorHex.substring(0, 6); + } + if (int.tryParse(colorHex, radix: 16) == null) { + colorHex = 'FFFFFF'; + } + return colorHex; + } + + String _fixOpacity(String opacity) { + // if opacity is 0 - 99, return it + // otherwise return 100 + final RegExp regex = RegExp('^(0|[1-9][0-9]?)'); + if (regex.hasMatch(opacity)) { + return opacity; + } else { + return '100'; + } + } + + void _submitCustomColorHex(String value) { + final String color = _combineColorHexAndOpacity( + widget.colorController.text, + widget.opacityController.text, + ); + widget.onSubmittedColorHex(color); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart index ee258a002d..4932eb8274 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -1,8 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'toolbar_animation.dart'; - class DesktopFloatingToolbar extends StatefulWidget { const DesktopFloatingToolbar({ super.key, @@ -41,15 +39,14 @@ class _DesktopFloatingToolbarState extends State { left: position!.left, top: position!.top, right: position!.right, - child: ToolbarAnimationWidget( - child: widget.child, - ), + child: widget.child, ); } _Position calculateSelectionMenuOffset( Rect rect, ) { + const toolbarHeight = 40, topLimit = toolbarHeight + 8; final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState); final menuWidth = isLongMenu ? 650.0 : 420.0; final editorOffset = @@ -57,7 +54,8 @@ class _DesktopFloatingToolbarState extends State { final editorSize = editorState.renderBox?.size ?? Size.zero; final editorRect = editorOffset & editorSize; final left = rect.left, leftStart = 50; - final top = rect.top < 40 ? rect.bottom + 40 : rect.top - 40; + final top = + rect.top < topLimit ? rect.bottom + topLimit : rect.top - topLimit; if (left + menuWidth > editorRect.right) { return _Position( editorRect.right - menuWidth, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index befd97fd52..d020468b37 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -1,9 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; @@ -78,7 +80,10 @@ class _FormatToolbarItem extends ToolbarItem { ? Color(0xFF282E3A) : Theme.of(context).iconTheme.color, ), - onPressed: () => editorState.toggleAttribute(name), + onPressed: () => editorState.toggleAttribute( + name, + selection: selection, + ), ); if (tooltipBuilder != null) { @@ -93,3 +98,46 @@ class _FormatToolbarItem extends ToolbarItem { }, ); } + +String getTooltipText(String id) { + switch (id) { + case 'underline': + return '${LocaleKeys.toolbar_underline.tr()}${shortcutTooltips( + '⌘ + U', + 'CTRL + U', + 'CTRL + U', + )}'; + case 'bold': + return '${LocaleKeys.toolbar_bold.tr()}${shortcutTooltips( + '⌘ + B', + 'CTRL + B', + 'CTRL + B', + )}'; + case 'italic': + return '${LocaleKeys.toolbar_italic.tr()}${shortcutTooltips( + '⌘ + I', + 'CTRL + I', + 'CTRL + I', + )}'; + case 'strikethrough': + return '${LocaleKeys.toolbar_strike.tr()}${shortcutTooltips( + '⌘ + SHIFT + S', + 'CTRL + SHIFT + S', + 'CTRL + SHIFT + S', + )}'; + case 'code': + return '${LocaleKeys.document_toolbar_inlineCode.tr()}${shortcutTooltips( + '⌘ + E', + 'CTRL + E', + 'CTRL + E', + )}'; + case 'align_left': + return LocaleKeys.document_toolbar_alignLeft.tr(); + case 'align_center': + return LocaleKeys.document_toolbar_alignCenter.tr(); + case 'align_right': + return LocaleKeys.document_toolbar_alignRight.tr(); + default: + return ''; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 76407482cd..39d216ed9c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -117,7 +118,7 @@ class _HighlightColorPickerWidgetState final List colors = []; final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); - nodes.allSatisfyInSelection(selection, (delta) { + final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { if (delta.everyAttributes((attr) => attr.isEmpty)) { return false; } @@ -130,7 +131,7 @@ class _HighlightColorPickerWidgetState }); final colorLength = colors.length; - if (colors.isEmpty) { + if (colors.isEmpty || !isHighLight) { return Container( width: 20, height: 4, @@ -158,7 +159,7 @@ class _HighlightColorPickerWidgetState final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); - nodes.allSatisfyInSelection(selection, (delta) { + final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { if (delta.everyAttributes((attr) => attr.isEmpty)) { return false; } @@ -186,7 +187,8 @@ class _HighlightColorPickerWidgetState child: ColorPicker( title: AppFlowyEditorL10n.current.highlightColor, showClearButton: showClearButton, - selectedColorHex: colors.length == 1 ? colors.first : null, + selectedColorHex: + (colors.length == 1 && isHighlight) ? colors.first : null, customColorHex: _customHighlightColorHex, colorOptions: generateHighlightColorOptions(), onSubmittedColorHex: (color, isCustomColor) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart index b531ecc0a2..2253ac7f75 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart @@ -17,7 +17,7 @@ final ToolbarItem customPlaceholderItem = ToolbarItem( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5), child: Container( width: 1, - color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 100), + color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 255), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index 3a5cabe6ee..659f637250 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -63,7 +63,7 @@ class _TextAlignActionListState extends State { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(-8.0, 2.0), + offset: const Offset(0, 2.0), onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 37ca253eb7..4b266eaa8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -1,7 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -114,7 +117,7 @@ class _TextColorPickerWidgetState extends State { final List colors = []; final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); - nodes.allSatisfyInSelection(selection, (delta) { + final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { if (delta.everyAttributes((attr) => attr.isEmpty)) { return false; } @@ -127,7 +130,7 @@ class _TextColorPickerWidgetState extends State { }); final colorLength = colors.length; - if (colors.isEmpty) { + if (colors.isEmpty || !isHighLight) { return Container( width: 20, height: 4, @@ -155,7 +158,7 @@ class _TextColorPickerWidgetState extends State { final List colors = []; final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); - nodes.allSatisfyInSelection(selection, (delta) { + final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { if (delta.everyAttributes((attr) => attr.isEmpty)) { return false; } @@ -182,9 +185,10 @@ class _TextColorPickerWidgetState extends State { ); return MouseRegion( child: ColorPicker( - title: AppFlowyEditorL10n.current.textColor, + title: LocaleKeys.document_toolbar_textColor.tr(), showClearButton: showClearButton, - selectedColorHex: colors.length == 1 ? colors.first : null, + selectedColorHex: + (colors.length == 1 && isHighLight) ? colors.first : null, customColorHex: _customColorHex, colorOptions: generateTextColorOptions(), onSubmittedColorHex: (color, isCustomColor) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart index 8d92b4a4b3..6ac63254f9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -1,15 +1,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import '../../editor_page.dart'; - const _kMoreOptionItemId = 'editor.more_option'; const kFontToolbarItemId = 'editor.font'; @@ -152,8 +153,7 @@ class _MoreOptionActionListState extends State { Widget buildPopoverContent() { final showFormula = onlyShowInSingleSelectionAndTextType(editorState); - final strikethroughColor = getStrikethroughColor(); - final Color? formulaColor = showFormula ? getFormulaColor() : null; + const fontColor = Color(0xff99A1A8); return MouseRegion( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, @@ -162,16 +162,32 @@ class _MoreOptionActionListState extends State { buildFontSelector(), buildCommandItem( MoreOptionCommand.strikethrough, - rightIcon: strikethroughColor != null - ? FlowySvg(FlowySvgs.toolbar_check_m) - : null, + rightIcon: FlowyText( + shortcutTooltips( + '⌘⇧S', + 'CTRL+SHIFT+S', + 'CTRL+SHIFT+S', + ).trim(), + color: fontColor, + fontSize: 12, + figmaLineHeight: 16, + fontWeight: FontWeight.w400, + ), ), if (showFormula) buildCommandItem( MoreOptionCommand.formula, - rightIcon: formulaColor != null - ? FlowySvg(FlowySvgs.toolbar_check_m) - : null, + rightIcon: FlowyText( + shortcutTooltips( + '⌘⇧E', + 'CTRL+SHIFT+E', + 'CTRL+SHIFT+E', + ).trim(), + color: fontColor, + fontSize: 12, + figmaLineHeight: 16, + fontWeight: FontWeight.w400, + ), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 4d1ef3d12a..d7be4b23fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -58,7 +58,7 @@ class _TextHeadingActionListState extends State { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(-8.0, 2.0), + offset: const Offset(0, 2.0), onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { @@ -220,10 +220,7 @@ enum TextHeadingCommand { node, state, level: level, - ); - await state.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, + keepSelection: true, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index e805689fc8..ffeecdcbbe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -85,7 +85,7 @@ class _SuggestionsActionListState extends State { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(-8.0, 2.0), + offset: const Offset(0, 2.0), onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { @@ -193,6 +193,7 @@ class _SuggestionsActionListState extends State { } Widget buildItem(SuggestionItem item) { + final isSelected = item.type == currentSuggestionItem.type; return SizedBox( height: 36, child: FlowyButton( @@ -204,6 +205,7 @@ class _SuggestionsActionListState extends State { fontWeight: FontWeight.w400, figmaLineHeight: 20, ), + rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () { item.onTap(widget.editorState); popoverController.close(); @@ -412,10 +414,7 @@ Future _turnInto(EditorState state, String type, {int? level}) async { node, state, level: level, - ); - await state.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, + keepSelection: true, ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ff09cb537e..1e50ff07ab 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2105,19 +2105,22 @@ }, "toolbar": { "resetToDefaultFont": "Reset to default", - "textSize": "Text Size", + "textSize": "Text size", + "textColor": "Text color", "h1": "Heading 1", "h2": "Heading 2", "h3": "Heading 3", - "alignLeft": "Align Left", - "alignRight": "Align Right", - "alignCenter": "Align Center", + "alignLeft": "Align left", + "alignRight": "Align right", + "alignCenter": "Align center", "link": "Link", - "textAlign": "Text Align", - "moreOptions": "More Options", + "textAlign": "Text align", + "moreOptions": "More options", "font": "Font", + "inlineCode": "Inline code", + "suggestions": "Suggestions", - "turnInto": "Turn Into", + "turnInto": "Turn into", "equation": "Equation" }, "errorBlock": { From 85b9aab0156110dc402bd1caf81ba28e0b41deda Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 18 Mar 2025 18:46:07 +0800 Subject: [PATCH 170/384] chore: update CHANGELOG.md (#7569) --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be3a1e8b8a..9db399773b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ # Release Notes +## Version 0.8.7 - 18/03/2025 +### New Features +- Made local AI free and integrated with Ollama +- Supported nested lists within callout and quote blocks +- Revamped the document's floating toolbar and added Turn Into +- Enabled custom icons in callout blocks +### Bug Fixes +- Fixed occasional incorrect positioning of the slash menu +- Improved AI Chat and AI Writers with various bug fixes +- Adjusted the columns block to match the width of the editor +- Fixed a potential segfault caused by infinite recursion in the trash view +- Resolved an issue where the first added cover might be invisible +- Fixed adding cover images via Unsplash + ## Version 0.8.6 - 06/03/2025 ### Bug Fixes - Fix the incorrect title positioning when adjusting the document width setting From f0d967f0e4e8a87171e80458458b57c1e5dfd36e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Mar 2025 10:23:38 +0800 Subject: [PATCH 171/384] chore: rename the local ai plugin --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 2 +- frontend/rust-lib/flowy-ai/src/local_ai/watch.rs | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6d45957104..b7d38b5dab 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6a12c1bad70fb9486c7aabf379d72d94cb73a2d5#6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4dad7f8744f6703f094b4c594aa4d65a487cc540#4dad7f8744f6703f094b4c594aa4d65a487cc540" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6a12c1bad70fb9486c7aabf379d72d94cb73a2d5#6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4dad7f8744f6703f094b4c594aa4d65a487cc540#4dad7f8744f6703f094b4c594aa4d65a487cc540" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c687278406..7ccaa020f4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6a12c1bad70fb9486c7aabf379d72d94cb73a2d5" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4dad7f8744f6703f094b4c594aa4d65a487cc540" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4dad7f8744f6703f094b4c594aa4d65a487cc540" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 489a67f69e..a162096f7d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -258,7 +258,7 @@ impl LocalAIResourceController { let mut config = OllamaPluginConfig::new( bin_path, - "ollama_ai_plugin".to_string(), + "af_ollama_plugin".to_string(), llm_setting.chat_model_name.clone(), llm_setting.embedding_model_name.clone(), Some(llm_setting.ollama_server_url.clone()), diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 895166d154..1d7be0daf9 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -101,18 +101,18 @@ pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { // Use LOCALAPPDATA for a user-specific installation path on Windows. let local_appdata = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Program Files".to_string()); - std::path::PathBuf::from(local_appdata).join("Programs\\appflowy_plugin\\ollama_ai_plugin.exe") + std::path::PathBuf::from(local_appdata).join("Programs\\appflowy_plugin\\af_ollama_plugin.exe") } #[cfg(target_os = "macos")] { - let offline_app = "ollama_ai_plugin"; + let offline_app = "af_ollama_plugin"; std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) } #[cfg(target_os = "linux")] { - let offline_app = "ollama_ai_plugin"; + let offline_app = "af_ollama_plugin"; std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) } } @@ -124,7 +124,7 @@ pub(crate) fn ollama_plugin_command_available() -> bool { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; let output = Command::new("cmd") - .args(&["/C", "where", "ollama_ai_plugin"]) + .args(&["/C", "where", "af_ollama_plugin"]) .creation_flags(CREATE_NO_WINDOW) .output(); if let Ok(output) = output { @@ -135,7 +135,7 @@ pub(crate) fn ollama_plugin_command_available() -> bool { // 2. Fallback: Check registry PATH for the executable let path_dirs = get_windows_path_dirs(); - let plugin_exe = "ollama_ai_plugin.exe"; // Adjust name if needed + let plugin_exe = "af_ollama_plugin.exe"; // Adjust name if needed path_dirs.iter().any(|dir| { let full_path = std::path::Path::new(dir).join(plugin_exe); @@ -147,7 +147,7 @@ pub(crate) fn ollama_plugin_command_available() -> bool { false } else { let output = Command::new("command") - .args(["-v", "ollama_ai_plugin"]) + .args(["-v", "af_ollama_plugin"]) .output(); match output { Ok(o) => !o.stdout.is_empty(), From e6b0c8ff054fad396af2907a81821128dc1a5b52 Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Wed, 19 Mar 2025 03:45:57 +0100 Subject: [PATCH 172/384] chore: update Arabic translations (#7571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 51 ++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index fddf5520db..243cf0c177 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -155,7 +155,8 @@ "charCountLabel": "عدد الأحرف: ", "createdAtLabel": "تم إنشاؤه: ", "syncedAtLabel": "تم المزامنة: ", - "saveAsNewPage": "حفظ الرسائل في الصفحة" + "saveAsNewPage": "حفظ الرسائل في الصفحة", + "saveAsNewPageDisabled": "لا توجد رسائل متاحة" }, "importPanel": { "textAndMarkdown": "نص و Markdown", @@ -260,7 +261,8 @@ "selectMessages": "حدد الرسائل", "nSelected": "{} تم التحديد", "allSelected": "جميعها محددة" - } + }, + "stopTooltip": "توقف عن التوليد" }, "trash": { "text": "المهملات", @@ -857,11 +859,13 @@ "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", + "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", "restartLocalAI": "إعادة تشغيل الذكاء الاصطناعي المحلي", "disableLocalAITitle": "تعطيل الذكاء الاصطناعي المحلي", "disableLocalAIDescription": "هل تريد تعطيل الذكاء الاصطناعي المحلي؟", "localAIToggleTitle": "التبديل لتفعيل أو تعطيل الذكاء الاصطناعي المحلي", + "localAIToggleSubTitle": "قم بتشغيل نماذج الذكاء الاصطناعي المحلية الأكثر تقدمًا داخل AppFlowy للحصول على أقصى درجات الخصوصية والأمان", "offlineAIInstruction1": "اتبع", "offlineAIInstruction2": "تعليمات", "offlineAIInstruction3": "لتفعيل الذكاء الاصطناعي دون اتصال بالإنترنت.", @@ -2127,7 +2131,21 @@ "morePages": "المزيد من الصفحات" }, "toolbar": { - "resetToDefaultFont": "إعادة تعيين إلى الافتراضي" + "resetToDefaultFont": "إعادة تعيين إلى الافتراضي", + "textSize": "حجم النص", + "h1": "العنوان 1", + "h2": "العنوان 2", + "h3": "العنوان 3", + "alignLeft": "محاذاة إلى اليسار", + "alignRight": "محاذاة إلى اليمين", + "alignCenter": "محاذاة إلى الوسط", + "link": "وصلة", + "textAlign": "محاذاة النص", + "moreOptions": "المزيد من الخيارات", + "font": "الخط", + "suggestions": "اقتراحات", + "turnInto": "تحول إلى", + "equation": "معادلة" }, "errorBlock": { "theBlockIsNotSupported": "الإصدار الحالي لا يدعم هذا الحقل.", @@ -2774,6 +2792,7 @@ "moreOptions": "المزيد من الخيارات", "collapse": "طي", "signInAgreement": "بالنقر فوق \"متابعة\" أعلاه، فإنك توافق على شروط استخدام AppFlowy", + "signInLocalAgreement": "من خلال النقر على \"البدء\" أعلاه، فإنك توافق على شروط وأحكام AppFlowy", "and": "و", "termOfUse": "شروط", "privacyPolicy": "سياسة الخصوصية", @@ -3172,6 +3191,30 @@ }, "autoUpdate": { "criticalUpdateTitle": "التحديث ضروري للمتابعة", - "criticalUpdateDescription": "لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق." + "criticalUpdateDescription": "لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق.", + "criticalUpdateButton": "تحديث", + "bannerUpdateTitle": "النسخة الجديدة متاحة!", + "bannerUpdateDescription": "احصل على أحدث الميزات والإصلاحات. انقر على \"تحديث\" للتثبيت الآن.", + "bannerUpdateButton": "تحديث", + "settingsUpdateTitle": "الإصدار الجديد ({newVersion}) متاح!", + "settingsUpdateDescription": "الإصدار الحالي: {currentVersion} (الإصدار الرسمي) → {newVersion}", + "settingsUpdateButton": "تحديث", + "settingsUpdateWhatsNew": "ما الجديد" + }, + "lockPage": { + "lockPage": "مقفل", + "reLockPage": "إعادة القفل", + "lockTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود. انقر لفتح القفل.", + "pageLockedToast": "الصفحة مقفلة. التعديل معطل حتى يفتحها أحد.", + "lockedOperationTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود." + }, + "suggestion": { + "accept": "يقبل", + "keep": "يحفظ", + "discard": "تجاهل", + "close": "يغلق", + "tryAgain": "حاول ثانية", + "rewrite": "إعادة كتابة", + "insertBelow": "أدخل أدناه" } } From 72b13dd9414156dba46b2982a8fbe3b9d365c57a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Mar 2025 10:48:30 +0800 Subject: [PATCH 173/384] chore: adjust UI --- .../setting_ai_view/local_ai_setting.dart | 2 +- .../local_ai_setting_panel.dart | 48 +++++-------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 949145afbd..feec20afdb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -45,7 +45,7 @@ class LocalAISetting extends StatelessWidget { collapsed: const SizedBox.shrink(), expanded: Column( children: [ - const VSpace(6), + const VSpace(12), DecoratedBox( decoration: BoxDecoration( color: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart index c379cf6089..5357db5c91 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -1,6 +1,5 @@ import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; -import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,40 +12,19 @@ class LocalAISettingPanel extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LocalAISettingPanelBloc(), - child: ExpandableNotifier( - initialExpanded: true, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: const SizedBox.shrink(), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: - BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // If the progress indicator is startLocalAIApp, then don't show the LLM model. - if (state.progressIndicator == - const LocalAIProgress.downloadLocalAIApp()) - const SizedBox.shrink() - else ...[ - OllamaSettingPage(), - VSpace(6), - PluginStateIndicator(), - ], - ], - ); - }, - ), - ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OllamaSettingPage(), + VSpace(6), + PluginStateIndicator(), + ], + ); + }, ), ), ); From 9230981e549445194fa20e65f332fdc4c1c34da8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Mar 2025 10:59:11 +0800 Subject: [PATCH 174/384] chore: remove test --- .../rust-lib/flowy-ai/src/local_ai/request.rs | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs b/frontend/rust-lib/flowy-ai/src/local_ai/request.rs index dd94b43041..6d4bd3289d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/request.rs @@ -116,46 +116,3 @@ async fn make_request( } Ok(response) } - -#[cfg(test)] -mod test { - use super::*; - use std::env::temp_dir; - #[tokio::test] - async fn retrieve_gpt4all_model_test() { - for url in [ - // "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", - "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", - // "https://huggingface.co/MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF/resolve/main/Mistral-7B-Instruct-v0.3.Q4_K_M.gguf?download=true", - ] { - let temp_dir = temp_dir().join("download_llm"); - if !temp_dir.exists() { - fs::create_dir(&temp_dir).await.unwrap(); - } - let file_name = "llm_model.gguf"; - let cancel_token = CancellationToken::new(); - let token = cancel_token.clone(); - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(120)).await; - token.cancel(); - }); - - let download_file = download_model( - url, - &temp_dir, - file_name, - Some(Arc::new(|a, b| { - println!("{}/{}", a, b); - })), - Some(cancel_token), - ).await.unwrap(); - - let file_path = temp_dir.join(file_name); - assert_eq!(download_file, file_path); - - println!("File path: {:?}", file_path); - assert!(file_path.exists()); - std::fs::remove_file(file_path).unwrap(); - } - } -} From 9db87944f2507159dd6b01df59dffb3fe11f3e34 Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 19 Mar 2025 11:23:07 +0800 Subject: [PATCH 175/384] fix: toolbar tooltip message is incorrect on Windows (#7572) * fix: some toolbar text display error * chore: change string id to enum string id --- .../custom_format_toolbar_items.dart | 39 +++++++------------ .../custom_hightlight_color_toolbar_item.dart | 7 ++-- .../custom_link_toolbar_item.dart | 6 +-- .../custom_placeholder_toolbar_item.dart | 7 ++-- .../custom_text_align_toolbar_item.dart | 6 +-- .../custom_text_color_toolbar_item.dart | 8 ++-- .../more_option_toolbar_item.dart | 8 ++-- .../text_heading_toolbar_item.dart | 6 +-- .../text_suggestions_toolbar_item.dart | 7 ++-- .../toolbar_item/toolbar_id_enum.dart | 19 +++++++++ .../document/presentation/editor_style.dart | 2 +- frontend/resources/translations/en.json | 1 - 12 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index d020468b37..a171c8ca4d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -10,29 +10,30 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'custom_placeholder_toolbar_item.dart'; +import 'toolbar_id_enum.dart'; final List customMarkdownFormatItems = [ _FormatToolbarItem( - id: 'bold', + id: ToolbarId.bold, name: 'bold', svg: FlowySvgs.toolbar_bold_m, ), group1PaddingItem, _FormatToolbarItem( - id: 'underline', + id: ToolbarId.underline, name: 'underline', svg: FlowySvgs.toolbar_underline_m, ), group1PaddingItem, _FormatToolbarItem( - id: 'italic', + id: ToolbarId.italic, name: 'italic', svg: FlowySvgs.toolbar_inline_italic_m, ), ]; final ToolbarItem customInlineCodeItem = _FormatToolbarItem( - id: 'code', + id: ToolbarId.code, name: 'code', svg: FlowySvgs.toolbar_inline_code_m, group: 2, @@ -40,12 +41,12 @@ final ToolbarItem customInlineCodeItem = _FormatToolbarItem( class _FormatToolbarItem extends ToolbarItem { _FormatToolbarItem({ - required String id, + required ToolbarId id, required String name, required FlowySvgData svg, super.group = 1, }) : super( - id: 'editor.$id', + id: id.id, isActive: showInAnyTextType, builder: ( context, @@ -89,8 +90,8 @@ class _FormatToolbarItem extends ToolbarItem { if (tooltipBuilder != null) { return tooltipBuilder( context, - id, - getTooltipText(id), + id.id, + _getTooltipText(id), child, ); } @@ -99,44 +100,32 @@ class _FormatToolbarItem extends ToolbarItem { ); } -String getTooltipText(String id) { +String _getTooltipText(ToolbarId id) { switch (id) { - case 'underline': + case ToolbarId.underline: return '${LocaleKeys.toolbar_underline.tr()}${shortcutTooltips( '⌘ + U', 'CTRL + U', 'CTRL + U', )}'; - case 'bold': + case ToolbarId.bold: return '${LocaleKeys.toolbar_bold.tr()}${shortcutTooltips( '⌘ + B', 'CTRL + B', 'CTRL + B', )}'; - case 'italic': + case ToolbarId.italic: return '${LocaleKeys.toolbar_italic.tr()}${shortcutTooltips( '⌘ + I', 'CTRL + I', 'CTRL + I', )}'; - case 'strikethrough': - return '${LocaleKeys.toolbar_strike.tr()}${shortcutTooltips( - '⌘ + SHIFT + S', - 'CTRL + SHIFT + S', - 'CTRL + SHIFT + S', - )}'; - case 'code': + case ToolbarId.code: return '${LocaleKeys.document_toolbar_inlineCode.tr()}${shortcutTooltips( '⌘ + E', 'CTRL + E', 'CTRL + E', )}'; - case 'align_left': - return LocaleKeys.document_toolbar_alignLeft.tr(); - case 'align_center': - return LocaleKeys.document_toolbar_alignCenter.tr(); - case 'align_right': - return LocaleKeys.document_toolbar_alignRight.tr(); default: return ''; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 39d216ed9c..2f2f75f0f0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -6,11 +6,12 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const _kHighlightColorItemId = 'editor.highlightColor'; +import 'toolbar_id_enum.dart'; + String? _customHighlightColorHex; final customHighlightColorItem = ToolbarItem( - id: _kHighlightColorItemId, + id: ToolbarId.highlightColor.id, group: 1, isActive: showInAnyTextType, builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => @@ -107,7 +108,7 @@ class _HighlightColorPickerWidgetState return widget.tooltipBuilder?.call( context, - _kHighlightColorItemId, + ToolbarId.highlightColor.id, AppFlowyEditorL10n.current.highlightColor, child, ) ?? diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index f6dc8bce7b..c7fa1a34cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -4,10 +4,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; -const _kLinkItemId = 'editor.link'; +import 'toolbar_id_enum.dart'; final customLinkItem = ToolbarItem( - id: _kLinkItemId, + id: ToolbarId.link.id, group: 4, isActive: onlyShowInSingleSelectionAndTextType, builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { @@ -39,7 +39,7 @@ final customLinkItem = ToolbarItem( if (tooltipBuilder != null) { return tooltipBuilder( context, - _kLinkItemId, + ToolbarId.highlightColor.id, AppFlowyEditorL10n.current.link, child, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart index 2253ac7f75..898f442891 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart @@ -4,11 +4,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const placeholderItemId = 'editor.placeholder'; -const paddingPlaceholderItemId = 'editor.padding_placeholder'; +import 'toolbar_id_enum.dart'; final ToolbarItem customPlaceholderItem = ToolbarItem( - id: placeholderItemId, + id: ToolbarId.placeholder.id, group: -1, isActive: (editorState) => true, builder: (context, __, ___, ____, _____) { @@ -28,7 +27,7 @@ ToolbarItem buildPaddingPlaceholderItem( bool Function(EditorState editorState)? isActive, }) => ToolbarItem( - id: paddingPlaceholderItemId, + id: ToolbarId.paddingPlaceHolder.id, group: group, isActive: isActive, builder: (context, __, ___, ____, _____) => HSpace(4), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index 659f637250..74198cb46b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -6,10 +6,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const _kTextAlignItemId = 'editor.text_align'; +import 'toolbar_id_enum.dart'; final ToolbarItem customTextAlignItem = ToolbarItem( - id: _kTextAlignItemId, + id: ToolbarId.textAlign.id, group: 4, isActive: onlyShowInSingleSelectionAndTextType, builder: ( @@ -114,7 +114,7 @@ class _TextAlignActionListState extends State { return widget.tooltipBuilder?.call( context, - _kTextAlignItemId, + ToolbarId.textAlign.id, LocaleKeys.document_toolbar_textAlign.tr(), child, ) ?? diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 4b266eaa8e..189cf64bd4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -7,12 +7,12 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'toolbar_id_enum.dart'; -const _kTextColorItemId = 'editor.textColor'; String? _customColorHex; final customTextColorItem = ToolbarItem( - id: _kTextColorItemId, + id: ToolbarId.textColor.id, group: 1, isActive: showInAnyTextType, builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => @@ -106,8 +106,8 @@ class _TextColorPickerWidgetState extends State { return widget.tooltipBuilder?.call( context, - _kTextColorItemId, - AppFlowyEditorL10n.current.textColor, + ToolbarId.textColor.id, + LocaleKeys.document_toolbar_textColor.tr(), child, ) ?? child; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart index 6ac63254f9..f844080a98 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -165,8 +165,8 @@ class _MoreOptionActionListState extends State { rightIcon: FlowyText( shortcutTooltips( '⌘⇧S', - 'CTRL+SHIFT+S', - 'CTRL+SHIFT+S', + 'Ctrl⇧S', + 'Ctrl⇧S', ).trim(), color: fontColor, fontSize: 12, @@ -180,8 +180,8 @@ class _MoreOptionActionListState extends State { rightIcon: FlowyText( shortcutTooltips( '⌘⇧E', - 'CTRL+SHIFT+E', - 'CTRL+SHIFT+E', + 'Ctrl⇧E', + 'Ctrl⇧E', ).trim(), color: fontColor, fontSize: 12, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index d7be4b23fd..4367ddb489 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -8,10 +8,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const _kTextHeadingItemId = 'editor.text_heading'; +import 'toolbar_id_enum.dart'; final ToolbarItem customTextHeadingItem = ToolbarItem( - id: _kTextHeadingItemId, + id: ToolbarId.textHeading.id, group: 1, isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, builder: ( @@ -109,7 +109,7 @@ class _TextHeadingActionListState extends State { return widget.tooltipBuilder?.call( context, - _kTextHeadingItemId, + ToolbarId.textHeading.id, LocaleKeys.document_toolbar_textSize.tr(), child, ) ?? diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index ffeecdcbbe..97245297b5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -14,8 +14,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'text_heading_toolbar_item.dart'; - -const _kSuggestionsItemId = 'editor.suggestions'; +import 'toolbar_id_enum.dart'; @visibleForTesting const kSuggestionsItemKey = ValueKey('SuggestionsItem'); @@ -24,7 +23,7 @@ const kSuggestionsItemKey = ValueKey('SuggestionsItem'); const kSuggestionsItemListKey = ValueKey('SuggestionsItemList'); final ToolbarItem suggestionsItem = ToolbarItem( - id: _kSuggestionsItemId, + id: ToolbarId.suggestions.id, group: 3, isActive: enableSuggestions, builder: ( @@ -159,7 +158,7 @@ class _SuggestionsActionListState extends State { return widget.tooltipBuilder?.call( context, - _kSuggestionsItemId, + ToolbarId.suggestions.id, currentSuggestionItem.title, child, ) ?? diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart new file mode 100644 index 0000000000..8a97bb6648 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart @@ -0,0 +1,19 @@ +enum ToolbarId { + bold, + underline, + italic, + code, + highlightColor, + textColor, + link, + placeholder, + paddingPlaceHolder, + textAlign, + moreOption, + textHeading, + suggestions, +} + +extension ToolbarIdExtension on ToolbarId { + String get id => 'editor.$name'; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 674b44e708..4b4b2d135a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -550,7 +550,7 @@ class EditorStyleCustomizer { style: context.tooltipTextStyle(), ), TextSpan( - text: (Platform.isMacOS ? '⌘+' : 'Ctrl+\\') + tooltip.$2, + text: (Platform.isMacOS ? '⌘+' : 'Ctrl+') + tooltip.$2, style: context.tooltipTextStyle()?.copyWith( color: Theme.of(context).hintColor, ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1e50ff07ab..55cc15d3ac 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2118,7 +2118,6 @@ "moreOptions": "More options", "font": "Font", "inlineCode": "Inline code", - "suggestions": "Suggestions", "turnInto": "Turn into", "equation": "Equation" From 461ac91b32aba54a8e8d02f57df92517276e9332 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:13:20 +0800 Subject: [PATCH 176/384] fix: continue writing empty text case (#7574) --- .../editor_plugins/ai/operations/ai_writer_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 2fb00c0b58..376757b1a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -318,7 +318,7 @@ class AiWriterCubit extends Cubit { end: cursorPosition, ).normalized; - String text = await editorState.getMarkdownInSelection(selection); + String text = (await editorState.getMarkdownInSelection(selection)).trim(); if (text.isEmpty) { if (state is! ReadyAiWriterState) { return; From 954aa48f522841c770ac261c55781c9c1723c556 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Mar 2025 14:17:17 +0800 Subject: [PATCH 177/384] chore: bump lai commit --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b7d38b5dab..bd95fd24c2 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4dad7f8744f6703f094b4c594aa4d65a487cc540#4dad7f8744f6703f094b4c594aa4d65a487cc540" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0136d80eeb4366913203db221b6a4aeae99dccd0#0136d80eeb4366913203db221b6a4aeae99dccd0" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4dad7f8744f6703f094b4c594aa4d65a487cc540#4dad7f8744f6703f094b4c594aa4d65a487cc540" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0136d80eeb4366913203db221b6a4aeae99dccd0#0136d80eeb4366913203db221b6a4aeae99dccd0" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 7ccaa020f4..eb94443c09 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4dad7f8744f6703f094b4c594aa4d65a487cc540" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4dad7f8744f6703f094b4c594aa4d65a487cc540" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0136d80eeb4366913203db221b6a4aeae99dccd0" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0136d80eeb4366913203db221b6a4aeae99dccd0" } From 6e4206a8e2a51b37d8cac9a642539b712806bb71 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 20 Mar 2025 11:41:49 +0800 Subject: [PATCH 178/384] chore: completion stream v2 --- .../lib/ai/service/appflowy_ai_service.dart | 6 +++ frontend/rust-lib/Cargo.lock | 28 +++++----- frontend/rust-lib/Cargo.toml | 8 +-- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 12 ++--- frontend/rust-lib/flowy-ai/src/completion.rs | 52 +++++++++++++------ frontend/rust-lib/flowy-ai/src/entities.rs | 10 ++-- .../src/middleware/chat_service_mw.rs | 20 +++---- .../flowy-server/src/af_cloud/impls/chat.rs | 2 +- 8 files changed, 85 insertions(+), 53 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 3c1d5c5a9d..723e11a17f 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -49,6 +49,9 @@ class AppFlowyAIService implements AIRepository { onProcess: onProcess, onEnd: onEnd, onError: onError, + onComment: (String text) async { + Log.info('Comment: $text'); + }, ); final records = history.map((record) => record.toPB()).toList(); @@ -80,12 +83,14 @@ abstract class CompletionStream { CompletionStream({ required this.onStart, required this.onProcess, + required this.onComment, required this.onEnd, required this.onError, }); final Future Function() onStart; final Future Function(String text) onProcess; + final Future Function(String text) onComment; final Future Function() onEnd; final void Function(AIError error) onError; } @@ -94,6 +99,7 @@ class AppFlowyCompletionStream extends CompletionStream { AppFlowyCompletionStream({ required super.onStart, required super.onProcess, + required super.onComment, required super.onEnd, required super.onError, }) { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index bd95fd24c2..6ec630d09d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "bytes", @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0136d80eeb4366913203db221b6a4aeae99dccd0#0136d80eeb4366913203db221b6a4aeae99dccd0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=76d7e7dc885b4d0bd9752aea7ff835de1d48de4e#76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0136d80eeb4366913203db221b6a4aeae99dccd0#0136d80eeb4366913203db221b6a4aeae99dccd0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=76d7e7dc885b4d0bd9752aea7ff835de1d48de4e#76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" dependencies = [ "anyhow", "cfg-if", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "futures-channel", "futures-util", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "async-trait", @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "bincode", "bytes", @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "app-error", "jsonwebtoken", @@ -3609,7 +3609,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "bytes", @@ -6176,7 +6176,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1634d17dd48488f6b55a7e0b874390343f18ebbf#1634d17dd48488f6b55a7e0b874390343f18ebbf" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index eb94443c09..fd4b19f6ec 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1634d17dd48488f6b55a7e0b874390343f18ebbf" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1634d17dd48488f6b55a7e0b874390343f18ebbf" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9587c0680f70a58d45c9bb012811bc702df5d213" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9587c0680f70a58d45c9bb012811bc702df5d213" } [profile.dev] opt-level = 0 @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0136d80eeb4366913203db221b6a4aeae99dccd0" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0136d80eeb4366913203db221b6a4aeae99dccd0" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" } diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 2043c123c0..d0107831ee 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,8 +1,7 @@ -use bytes::Bytes; pub use client_api::entity::ai_dto::{ - AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionRecord, CompletionType, - CreateChatContext, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, - RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, + AppFlowyOfflineAI, CompleteTextParams, CompletionMessage, CompletionMetadata, CompletionType, + CreateChatContext, CustomPrompt, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, + OutputLayout, RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ @@ -10,7 +9,8 @@ pub use client_api::entity::chat_dto::{ MessageCursor, RepeatedChatMessage, UpdateChatParams, }; pub use client_api::entity::QuestionStreamValue; -use client_api::error::AppResponseError; +pub use client_api::entity::*; +pub use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; use flowy_error::FlowyError; use futures::stream::BoxStream; use lib_infra::async_trait::async_trait; @@ -20,7 +20,7 @@ use std::path::Path; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; -pub type StreamComplete = BoxStream<'static, Result>; +pub type StreamComplete = BoxStream<'static, Result>; #[async_trait] pub trait ChatCloudService: Send + Sync + 'static { async fn create_chat( diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 4ed073f0ca..fa79308b4f 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -4,7 +4,8 @@ use allo_isolate::Isolate; use dashmap::DashMap; use flowy_ai_pub::cloud::{ - ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionType, + ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionStreamValue, CompletionType, + CustomPrompt, }; use flowy_error::{FlowyError, FlowyResult}; @@ -37,6 +38,15 @@ impl AICompletion { &self, complete: CompleteTextPB, ) -> FlowyResult { + if matches!(complete.completion_type, CompletionTypePB::CustomPrompt) + && complete.custom_prompt.is_none() + { + return Err( + FlowyError::invalid_data() + .with_context("custom_prompt is required when completion_type is CustomPrompt"), + ); + } + let workspace_id = self .user_service .upgrade() @@ -95,6 +105,7 @@ impl CompletionTask { CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, CompletionTypePB::ExplainSelected => CompletionType::Explain, CompletionTypePB::UserQuestion => CompletionType::UserQuestion, + CompletionTypePB::CustomPrompt => CompletionType::CustomPrompt, }; let _ = sink.send("start:".to_string()).await; @@ -103,12 +114,15 @@ impl CompletionTask { let params = CompleteTextParams { text: self.context.text, completion_type: Some(complete_type), - custom_prompt: None, metadata: Some(CompletionMetadata { object_id: self.context.object_id, workspace_id: Some(self.workspace_id.clone()), rag_ids: Some(self.context.rag_ids), completion_history, + custom_prompt: self + .context + .custom_prompt + .map(|v| CustomPrompt { system: v }), }), format, }; @@ -124,20 +138,26 @@ impl CompletionTask { return; }, result = stream.next() => { - match result { - Some(Ok(data)) => { - let s = String::from_utf8(data.to_vec()).unwrap_or_default(); - let _ = sink.send(format!("data:{}", s)).await; - }, - Some(Err(error)) => { - handle_error(&mut sink, error).await; - return; - }, - None => { - let _ = sink.send(format!("finish:{}", self.task_id)).await; - return; - }, - } + match result { + Some(Ok(data)) => { + match data { + CompletionStreamValue::Answer{ value } => { + let _ = sink.send(format!("data:{}", value)).await; + } + CompletionStreamValue::Comment{ value } => { + let _ = sink.send(format!("comment:{}", value)).await; + } + } + }, + Some(Err(error)) => { + handle_error(&mut sink, error).await; + return; + }, + None => { + let _ = sink.send(format!("finish:{}", self.task_id)).await; + return; + }, + } } } }, diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 4b5e91db11..70a8b5ff58 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; use flowy_ai_pub::cloud::{ - ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionRecord, LLMModel, OutputContent, + ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; @@ -361,6 +361,9 @@ pub struct CompleteTextPB { #[pb(index = 7)] pub history: Vec, + + #[pb(index = 8, one_of)] + pub custom_prompt: Option, } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -379,6 +382,7 @@ pub enum CompletionTypePB { ImproveWriting = 4, MakeShorter = 5, MakeLonger = 6, + CustomPrompt = 7, } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -390,9 +394,9 @@ pub struct CompletionRecordPB { pub content: String, } -impl From<&CompletionRecordPB> for CompletionRecord { +impl From<&CompletionRecordPB> for CompletionMessage { fn from(value: &CompletionRecordPB) -> Self { - CompletionRecord { + CompletionMessage { role: match value.role { // Coerce ChatMessageTypePB::System to AI ChatMessageTypePB::System => "ai".to_string(), diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index d03b1d88c2..8e69f14d20 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -9,10 +9,10 @@ use appflowy_plugin::error::PluginError; use std::collections::HashMap; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RelatedQuestion, - RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, - SubscriptionPlan, UpdateChatParams, + AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, + ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, LocalAIConfig, + MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, + ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; @@ -194,7 +194,6 @@ impl ChatCloudService for AICloudServiceMiddleware { let content = self.get_message_record(question_message_id)?.content; match self.local_ai.ask_question(chat_id, &content).await { Ok(answer) => { - // TODO(nathan): metadata let message = self .cloud_service .create_answer(workspace_id, chat_id, &answer, question_message_id, None) @@ -278,17 +277,20 @@ impl ChatCloudService for AICloudServiceMiddleware { if self.local_ai.is_running() { match self .local_ai - .complete_text( + .complete_text_v2( ¶ms.text, params.completion_type.unwrap() as u8, Some(json!(params.format)), + Some(json!(params.metadata)), ) .await { Ok(stream) => Ok( - stream - .map_err(|err| FlowyError::local_ai().with_context(err)) - .boxed(), + CompletionStream::new( + stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), + ) + .map_err(FlowyError::from) + .boxed(), ), Err(err) => { self.handle_plugin_error(err); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index c7077dc061..5417f06a65 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -193,7 +193,7 @@ where let stream = self .inner .try_get_client()? - .stream_completion_text(workspace_id, params) + .stream_completion_v2(workspace_id, params) .await .map_err(FlowyError::from)? .map_err(FlowyError::from); From f413b9e070fc4a81ba8c4f1a96f4dce243af3ff7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 20 Mar 2025 11:44:02 +0800 Subject: [PATCH 179/384] chore: callback --- .../appflowy_flutter/lib/ai/service/appflowy_ai_service.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 723e11a17f..e3093b6bbf 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -151,6 +151,10 @@ class AppFlowyCompletionStream extends CompletionStream { await onProcess(event.substring(5)); } + if (event.startsWith("comment:")) { + await onComment(event.substring(8)); + } + if (event.startsWith("finish:")) { await onEnd(); } From 566e7b2f40b1247f0844df412f3fb9f5c8f5cbbb Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:50:25 +0800 Subject: [PATCH 180/384] feat: allow user scroll during generation (#7559) * chore: add keep alive to ai writer block component * chore: allow user scrolling during ai writer generation * chore: pull ai writer cubit upwards * test: fix unit tests * chore: clear selection --- .../lib/plugins/document/document_page.dart | 32 +- .../ai/ai_writer_block_component.dart | 150 ++---- .../ai_writer_block_operations.dart | 25 +- .../ai/operations/ai_writer_cubit.dart | 470 +++++++++--------- .../widgets/ai_writer_gesture_detector.dart | 5 +- .../ai/widgets/ai_writer_scroll_wrapper.dart | 223 +++++++++ .../base/markdown_text_robot.dart | 10 +- .../appflowy_flutter/lib/util/throttle.dart | 4 + .../ai_writer_test/ai_writer_bloc_test.dart | 51 +- 9 files changed, 580 insertions(+), 390 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 46ef2ca7f2..8716bb7ae2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; @@ -55,8 +56,6 @@ class _DocumentPageState extends State Selection? initialSelection; late final documentBloc = DocumentBloc(documentId: widget.view.id) ..add(const DocumentEvent.initial()); - late final viewBloc = ViewBloc(view: widget.view) - ..add(const ViewEvent.initial()); @override void initState() { @@ -68,7 +67,6 @@ class _DocumentPageState extends State void dispose() { WidgetsBinding.instance.removeObserver(this); documentBloc.close(); - viewBloc.close(); super.dispose(); } @@ -93,7 +91,11 @@ class _DocumentPageState extends State value: ViewLockStatusBloc(view: widget.view) ..add(ViewLockStatusEvent.initial()), ), - BlocProvider.value(value: viewBloc), + BlocProvider( + create: (context) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + lazy: false, + ), ], child: BlocConsumer( listenWhen: (prev, curr) => curr.isLocked != prev.isLocked, @@ -126,14 +128,20 @@ class _DocumentPageState extends State return const SizedBox.shrink(); } - return BlocListener( - listener: (context, state) { - editorState.editable = !state.isLocked; - }, - child: - BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: onNotificationAction, + return MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) => + editorState.editable = !state.isLocked, + ), + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: onNotificationAction, + ), + ], + child: AiWriterScrollWrapper( + viewId: widget.view.id, + editorState: editorState, child: buildEditorPage(context, state), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index aee2e2cb50..c1b533ccca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -2,11 +2,8 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; @@ -19,7 +16,6 @@ import 'operations/ai_writer_cubit.dart'; import 'operations/ai_writer_entities.dart'; import 'operations/ai_writer_node_extension.dart'; import 'suggestion_action_bar.dart'; -import 'widgets/ai_writer_gesture_detector.dart'; class AiWriterBlockKeys { const AiWriterBlockKeys._(); @@ -98,18 +94,11 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget { } class _AIWriterBlockComponentState extends State { - final key = GlobalKey(); final textController = TextEditingController(); final overlayController = OverlayPortalController(); final layerLink = LayerLink(); late final editorState = context.read(); - late final aiWriterCubit = AiWriterCubit( - documentId: context.read().documentId, - editorState: editorState, - getAiWriterNode: () => widget.node, - initialCommand: widget.node.aiWriterCommand, - ); @override void initState() { @@ -118,7 +107,7 @@ class _AIWriterBlockComponentState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { overlayController.show(); if (!widget.node.isAiWriterInitialized) { - aiWriterCubit.init(); + context.read().register(widget.node); } }); } @@ -126,7 +115,6 @@ class _AIWriterBlockComponentState extends State { @override void dispose() { textController.dispose(); - aiWriterCubit.close(); super.dispose(); } @@ -136,85 +124,42 @@ class _AIWriterBlockComponentState extends State { return const SizedBox.shrink(); } - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: aiWriterCubit, - ), - BlocProvider( - create: (_) => AIPromptInputBloc( - predefinedFormat: null, - ), - ), - ], + return BlocProvider( + create: (_) => AIPromptInputBloc( + predefinedFormat: null, + ), child: LayoutBuilder( builder: (context, constraints) { - return BlocListener( - listener: (context, state) { - if (state is FailedContinueWritingAiWriterState) { - showConfirmDialog( - context: context, - title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: LocaleKeys - .ai_continueWritingEmptyDocumentDescription - .tr(), - onConfirm: state.onConfirm, - ); - } else if (state is DiscardResponseAiWriterState) { - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: state.onDiscard, - onCancel: () {}, - ); - } - }, - child: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return Stack( - children: [ - BlocBuilder( - builder: (context, state) { - return AiWriterGestureDetector( - behavior: state is GeneratingAiWriterState - ? HitTestBehavior.opaque - : HitTestBehavior.translucent, - onPointerEvent: onTapOutside, - ); - }, + return OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return Center( + child: CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + child: Container( + padding: const EdgeInsets.only( + left: 40.0, + bottom: 16.0, ), - CompositedTransformFollower( - link: layerLink, - showWhenUnlinked: false, - child: Container( - padding: const EdgeInsets.only( - left: 40.0, - bottom: 16.0, - ), - width: constraints.maxWidth, - child: OverlayContent( - editorState: editorState, - node: widget.node, - ), - ), + width: constraints.maxWidth, + child: OverlayContent( + editorState: editorState, + node: widget.node, ), - ], - ); - }, - child: CompositedTransformTarget( - link: layerLink, - child: BlocBuilder( - builder: (context, state) { - return SizedBox( - key: key, - width: double.infinity, - ); - }, + ), ), + ); + }, + child: CompositedTransformTarget( + link: layerLink, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + width: double.infinity, + height: 1.0, + ); + }, ), ), ); @@ -222,26 +167,6 @@ class _AIWriterBlockComponentState extends State { ), ); } - - void onTapOutside() { - if (aiWriterCubit.hasUnusedResponse()) { - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () => aiWriterCubit - ..stopStream() - ..exit(), - onCancel: () {}, - ); - } else { - aiWriterCubit - ..stopStream() - ..exit(); - } - } } class OverlayContent extends StatelessWidget { @@ -258,6 +183,9 @@ class OverlayContent extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + if (state is IdleAiWriterState) { + return const SizedBox.shrink(); + } final selection = node.aiWriterSelection; final showSuggestionPopup = state is ReadyAiWriterState && !state.isFirstRun; @@ -337,7 +265,9 @@ class OverlayContent extends StatelessWidget { alignment: AlignmentDirectional.centerStart, child: FlowyText( - state.command.i18n, + (state as RegisteredAiWriter) + .command + .i18n, fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF666D76), @@ -421,7 +351,9 @@ class OverlayContent extends StatelessWidget { context.read().runCommand( command, - showPredefinedFormats ? predefinedFormat : null, + predefinedFormat: + showPredefinedFormats ? predefinedFormat : null, + isFirstRun: false, ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart index c82b39e241..c503042339 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart @@ -4,7 +4,26 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import '../ai_writer_block_component.dart'; import 'ai_writer_entities.dart'; -Future removeAiWriterNode(EditorState editorState, Node node) async { +Future setAiWriterNodeIsInitialized( + EditorState editorState, + Node node, +) async { + final transaction = editorState.transaction + ..updateNode(node, { + AiWriterBlockKeys.isInitialized: true, + }) + ..afterSelection = null; + + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + ); +} + +Future removeAiWriterNode( + EditorState editorState, + Node node, +) async { final transaction = editorState.transaction..deleteNode(node); await editorState.apply( transaction, @@ -88,10 +107,6 @@ Position ensurePreviousNodeIsEmptyParagraph( position = Position(path: previous.path); } - transaction.updateNode(aiWriterNode, { - AiWriterBlockKeys.isInitialized: true, - }); - return position; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 376757b1a7..133b0b5562 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -10,7 +10,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import '../../base/markdown_text_robot.dart'; import 'ai_writer_block_operations.dart'; @@ -21,28 +20,24 @@ class AiWriterCubit extends Cubit { AiWriterCubit({ required this.documentId, required this.editorState, - required this.getAiWriterNode, - required this.initialCommand, + this.onCreateNode, + this.onRemoveNode, + this.onAppendToDocument, AppFlowyAIService? aiService, }) : _aiService = aiService ?? AppFlowyAIService(), _textRobot = MarkdownTextRobot(editorState: editorState), selectedSourcesNotifier = ValueNotifier([documentId]), - super( - ReadyAiWriterState( - initialCommand, - isFirstRun: true, - ), - ) { - HardwareKeyboard.instance.addHandler(_cancelShortcutHandler); - editorState.service.keyboardService?.disableShortcuts(); - } + super(IdleAiWriterState()); final String documentId; final EditorState editorState; - final Node Function() getAiWriterNode; - final AiWriterCommand initialCommand; final AppFlowyAIService _aiService; final MarkdownTextRobot _textRobot; + final void Function()? onCreateNode; + final void Function()? onRemoveNode; + final void Function()? onAppendToDocument; + + Node? aiWriterNode; final List records = []; final ValueNotifier> selectedSourcesNotifier; @@ -52,85 +47,58 @@ class AiWriterCubit extends Cubit { @override Future close() async { selectedSourcesNotifier.dispose(); - HardwareKeyboard.instance.removeHandler(_cancelShortcutHandler); - editorState.service.keyboardService?.enableShortcuts(); await super.close(); } - void init() { - runCommand(initialCommand, null, isImmediateRun: true); + void register(Node node) async { + aiWriterNode = node; + onCreateNode?.call(); + + await setAiWriterNodeIsInitialized(editorState, node); + + final command = node.aiWriterCommand; + if (command == AiWriterCommand.userQuestion) { + emit(ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true)); + } else { + runCommand(command, isFirstRun: true); + } } - void submit( - String prompt, - PredefinedFormat? format, - ) async { - final command = AiWriterCommand.userQuestion; - final node = getAiWriterNode(); + Future exit() async { + await _textRobot.discard(); + _textRobot.reset(); + onRemoveNode?.call(); + emit(IdleAiWriterState()); - _previousPrompt = (prompt, format); - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: prompt, - format: format, - sourceIds: selectedSourcesNotifier.value, - completionType: command.toCompletionType(), - history: records, - onStart: () async { - final transaction = editorState.transaction; - final position = - ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); - transaction.afterSelection = null; - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - ); - _textRobot.start(position: position); - records.add( - AiWriterRecord.user(content: prompt), - ); - }, - onProcess: (text) async { - await _textRobot.appendMarkdownText( - text, - attributes: ApplySuggestionFormatType.replace.attributes, - ); - }, - onEnd: () async { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit(ReadyAiWriterState(command, isFirstRun: false)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onError: (error) async { - emit(ErrorAiWriterState(state.command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - ); - - if (stream != null) { - emit( - GeneratingAiWriterState( - command, - taskId: stream.$1, - ), + if (aiWriterNode != null) { + final selection = aiWriterNode!.aiWriterSelection; + if (selection == null) { + return; + } + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.clear, ); + await editorState.apply( + transaction, + options: const ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + withUpdateSelection: false, + ); + await removeAiWriterNode(editorState, aiWriterNode!); + aiWriterNode = null; } } void runCommand( - AiWriterCommand command, - PredefinedFormat? predefinedFormat, { - bool isImmediateRun = false, + AiWriterCommand command, { + required bool isFirstRun, + PredefinedFormat? predefinedFormat, bool isRetry = false, }) async { switch (command) { @@ -138,7 +106,7 @@ class AiWriterCubit extends Cubit { await _startContinueWriting( command, predefinedFormat, - isImmediateRun: isImmediateRun, + isImmediateRun: isFirstRun, ); break; case AiWriterCommand.fixSpellingAndGrammar: @@ -158,96 +126,79 @@ class AiWriterCubit extends Cubit { } } - void stopStream() async { - if (state is! GeneratingAiWriterState) { - return; - } - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - final generatingState = state as GeneratingAiWriterState; - await AIEventStopCompleteText( - CompleteTextTaskPB( - taskId: generatingState.taskId, - ), - ).send(); - emit( - ReadyAiWriterState( - state.command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - } + Future stopStream() async { + if (state is GeneratingAiWriterState) { + final generatingState = state as GeneratingAiWriterState; - void exit() async { - await _textRobot.discard(); - final selection = getAiWriterNode().aiWriterSelection; - if (selection == null) { - return; + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + + await AIEventStopCompleteText( + CompleteTextTaskPB( + taskId: generatingState.taskId, + ), + ).send(); + + emit( + ReadyAiWriterState( + generatingState.command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); } - final transaction = editorState.transaction; - formatSelection( - editorState, - selection, - transaction, - ApplySuggestionFormatType.clear, - ); - await editorState.apply( - transaction, - options: const ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, - ); - await removeAiWriterNode(editorState, getAiWriterNode()); } void runResponseAction( SuggestionAction action, [ PredefinedFormat? predefinedFormat, ]) async { - if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { - await _textRobot.discard(); - _textRobot.reset(); - runCommand(state.command, predefinedFormat, isRetry: true); + if (aiWriterNode == null) { return; } - final selection = getAiWriterNode().aiWriterSelection; + if (state is! RegisteredAiWriter) { + return; + } + + final command = (state as RegisteredAiWriter).command; + + if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { + await _textRobot.discard(); + _textRobot.reset(); + runCommand( + command, + predefinedFormat: predefinedFormat, + isRetry: true, + isFirstRun: false, + ); + return; + } + + final selection = aiWriterNode!.aiWriterSelection; if (selection == null) { return; } if (action case SuggestionAction.discard || SuggestionAction.close) { - await _textRobot.discard(); + await exit(); + return; + } - final transaction = editorState.transaction; - formatSelection( - editorState, - selection, - transaction, - ApplySuggestionFormatType.clear, - ); + if (action case SuggestionAction.accept) { + await _textRobot.persist(); + final nodes = editorState.getNodesInSelection(selection); + final transaction = editorState.transaction..deleteNodes(nodes); await editorState.apply( transaction, options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, ); } - if (action case SuggestionAction.accept || SuggestionAction.keep) { + if (action case SuggestionAction.keep) { await _textRobot.persist(); - - if (acceptReplacesOriginal) { - final nodes = editorState.getNodesInSelection(selection); - final transaction = editorState.transaction..deleteNodes(nodes); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - withUpdateSelection: false, - ); - } } if (action case SuggestionAction.insertBelow) { @@ -256,7 +207,7 @@ class AiWriterCubit extends Cubit { final transaction = editorState.transaction; final position = ensurePreviousNodeIsEmptyParagraph( editorState, - getAiWriterNode(), + aiWriterNode!, transaction, ); transaction.afterSelection = null; @@ -266,6 +217,7 @@ class AiWriterCubit extends Cubit { inMemoryUpdate: true, recordUndo: false, ), + withUpdateSelection: false, ); _textRobot.start(position: position); await _textRobot.persist(markdownText: readyState.markdownText); @@ -287,7 +239,9 @@ class AiWriterCubit extends Cubit { ); } - await removeAiWriterNode(editorState, getAiWriterNode()); + await removeAiWriterNode(editorState, aiWriterNode!); + aiWriterNode = null; + emit(IdleAiWriterState()); } bool hasUnusedResponse() { @@ -302,14 +256,87 @@ class AiWriterCubit extends Cubit { }; } + void submit( + String prompt, + PredefinedFormat? format, + ) async { + if (aiWriterNode == null) { + return; + } + final command = AiWriterCommand.userQuestion; + _previousPrompt = (prompt, format); + + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: prompt, + format: format, + history: records, + sourceIds: selectedSourcesNotifier.value, + completionType: command.toCompletionType(), + onStart: () async { + final transaction = editorState.transaction; + final position = ensurePreviousNodeIsEmptyParagraph( + editorState, + aiWriterNode!, + transaction, + ); + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + withUpdateSelection: false, + ); + _textRobot.start(position: position); + records.add( + AiWriterRecord.user(content: prompt), + ); + }, + onProcess: (text) async { + await _textRobot.appendMarkdownText( + text, + updateSelection: false, + attributes: ApplySuggestionFormatType.replace.attributes, + ); + onAppendToDocument?.call(); + }, + onEnd: () async { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit(ReadyAiWriterState(command, isFirstRun: false)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); + }, + onError: (error) async { + emit(ErrorAiWriterState(command, error: error)); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); + }, + ); + + if (stream != null) { + emit( + GeneratingAiWriterState( + command, + taskId: stream.$1, + ), + ); + } + } + Future _startContinueWriting( AiWriterCommand command, PredefinedFormat? predefinedFormat, { required bool isImmediateRun, }) async { - final node = getAiWriterNode(); - - final cursorPosition = getAiWriterNode().aiWriterSelection?.start; + if (aiWriterNode == null) { + return; + } + final cursorPosition = aiWriterNode?.aiWriterSelection?.start; if (cursorPosition == null) { return; } @@ -320,28 +347,25 @@ class AiWriterCubit extends Cubit { String text = (await editorState.getMarkdownInSelection(selection)).trim(); if (text.isEmpty) { - if (state is! ReadyAiWriterState) { - return; - } final view = await ViewBackendService.getView(documentId).toNullable(); if (view == null || view.name.isEmpty || view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) { - final readyState = state as ReadyAiWriterState; + final stateCopy = state; emit( - FailedContinueWritingAiWriterState( + DocumentContentEmptyAiWriterState( command, onConfirm: () { if (isImmediateRun) { - removeAiWriterNode(editorState, node); + removeAiWriterNode(editorState, aiWriterNode!); } }, ), ); - emit(readyState); + emit(stateCopy); return; } else { - text += view.name; + text = view.name; } } @@ -352,8 +376,11 @@ class AiWriterCubit extends Cubit { history: records, onStart: () async { final transaction = editorState.transaction; - final position = - ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + final position = ensurePreviousNodeIsEmptyParagraph( + editorState, + aiWriterNode!, + transaction, + ); transaction.afterSelection = null; await editorState.apply( transaction, @@ -361,14 +388,17 @@ class AiWriterCubit extends Cubit { inMemoryUpdate: true, recordUndo: false, ), + withUpdateSelection: false, ); _textRobot.start(position: position); }, onProcess: (text) async { await _textRobot.appendMarkdownText( text, + updateSelection: false, attributes: ApplySuggestionFormatType.replace.attributes, ); + onAppendToDocument?.call(); }, onEnd: () async { if (state case GeneratingAiWriterState _) { @@ -399,8 +429,10 @@ class AiWriterCubit extends Cubit { AiWriterCommand command, PredefinedFormat? predefinedFormat, ) async { - final node = getAiWriterNode(); - final selection = node.aiWriterSelection; + if (aiWriterNode == null) { + return; + } + final selection = aiWriterNode?.aiWriterSelection; if (selection == null) { return; } @@ -420,8 +452,11 @@ class AiWriterCubit extends Cubit { transaction, ApplySuggestionFormatType.original, ); - final position = - ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + final position = ensurePreviousNodeIsEmptyParagraph( + editorState, + aiWriterNode!, + transaction, + ); transaction.afterSelection = null; await editorState.apply( transaction, @@ -429,14 +464,17 @@ class AiWriterCubit extends Cubit { inMemoryUpdate: true, recordUndo: false, ), + withUpdateSelection: false, ); _textRobot.start(position: position); }, onProcess: (text) async { await _textRobot.appendMarkdownText( text, + updateSelection: false, attributes: ApplySuggestionFormatType.replace.attributes, ); + onAppendToDocument?.call(); }, onEnd: () async { if (state is GeneratingAiWriterState) { @@ -472,8 +510,10 @@ class AiWriterCubit extends Cubit { AiWriterCommand command, PredefinedFormat? predefinedFormat, ) async { - final node = getAiWriterNode(); - final selection = node.aiWriterSelection; + if (aiWriterNode == null) { + return; + } + final selection = aiWriterNode?.aiWriterSelection; if (selection == null) { return; } @@ -524,101 +564,71 @@ class AiWriterCubit extends Cubit { ); } } +} - bool _cancelShortcutHandler(KeyEvent event) { - if (event is! KeyUpEvent) { - return false; - } - - switch (event.logicalKey) { - case LogicalKeyboardKey.escape: - if (state case GeneratingAiWriterState _) { - stopStream(); - } else if (hasUnusedResponse()) { - final saveState = state; - emit( - FailedContinueWritingAiWriterState( - state.command, - onConfirm: () { - stopStream(); - exit(); - }, - ), - ); - emit(saveState); - } else { - stopStream(); - exit(); - } - return true; - case LogicalKeyboardKey.keyC - when HardwareKeyboard.instance.logicalKeysPressed - .contains(LogicalKeyboardKey.controlLeft): - if (state case GeneratingAiWriterState _) { - stopStream(); - } - return true; - default: - break; - } - - return false; - } +mixin RegisteredAiWriter { + AiWriterCommand get command; } sealed class AiWriterState { - const AiWriterState(this.command); - - final AiWriterCommand command; + const AiWriterState(); } -class ReadyAiWriterState extends AiWriterState { +class IdleAiWriterState extends AiWriterState { + const IdleAiWriterState(); +} + +class ReadyAiWriterState extends AiWriterState with RegisteredAiWriter { const ReadyAiWriterState( - super.command, { + this.command, { required this.isFirstRun, this.markdownText = '', }); + @override + final AiWriterCommand command; + final bool isFirstRun; final String markdownText; } -class GeneratingAiWriterState extends AiWriterState { +class GeneratingAiWriterState extends AiWriterState with RegisteredAiWriter { const GeneratingAiWriterState( - super.command, { + this.command, { required this.taskId, this.progress = '', this.markdownText = '', }); + @override + final AiWriterCommand command; + final String taskId; final String progress; final String markdownText; } -class ErrorAiWriterState extends AiWriterState { +class ErrorAiWriterState extends AiWriterState with RegisteredAiWriter { const ErrorAiWriterState( - super.command, { + this.command, { required this.error, }); + @override + final AiWriterCommand command; + final AIError error; } -class FailedContinueWritingAiWriterState extends AiWriterState { - const FailedContinueWritingAiWriterState( - super.command, { +class DocumentContentEmptyAiWriterState extends AiWriterState + with RegisteredAiWriter { + const DocumentContentEmptyAiWriterState( + this.command, { required this.onConfirm, }); + @override + final AiWriterCommand command; + final void Function() onConfirm; } - -class DiscardResponseAiWriterState extends AiWriterState { - const DiscardResponseAiWriterState( - super.command, { - required this.onDiscard, - }); - - final void Function() onDiscard; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart index 84f785335b..8a691acdfc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart @@ -21,7 +21,10 @@ class AiWriterGestureDetector extends StatelessWidget { TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), - (instance) => instance..onTapDown = (_) => onPointerEvent(), + (instance) => instance + ..onTapDown = ((_) => onPointerEvent()) + ..onSecondaryTapDown = ((_) => onPointerEvent()) + ..onTertiaryTapDown = ((_) => onPointerEvent()), ), ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart new file mode 100644 index 0000000000..4c04baf557 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -0,0 +1,223 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/throttle.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../operations/ai_writer_cubit.dart'; +import 'ai_writer_gesture_detector.dart'; + +class AiWriterScrollWrapper extends StatefulWidget { + const AiWriterScrollWrapper({ + super.key, + required this.viewId, + required this.editorState, + required this.child, + }); + + final String viewId; + final EditorState editorState; + final Widget child; + + @override + State createState() => _AiWriterScrollWrapperState(); +} + +class _AiWriterScrollWrapperState extends State { + final overlayController = OverlayPortalController(); + late final throttler = Throttler(); + late final aiWriterCubit = AiWriterCubit( + documentId: widget.viewId, + editorState: widget.editorState, + onCreateNode: () { + aiWriterRegistered = true; + widget.editorState.service.keyboardService?.disableShortcuts(); + HardwareKeyboard.instance.addHandler(cancelShortcutHandler); + }, + onRemoveNode: () { + aiWriterRegistered = false; + HardwareKeyboard.instance.removeHandler(cancelShortcutHandler); + widget.editorState.service.keyboardService?.enableShortcuts(); + }, + onAppendToDocument: onAppendToDocument, + ); + + bool userHasScrolled = false; + bool aiWriterRegistered = false; + + @override + void initState() { + super.initState(); + overlayController.show(); + } + + @override + void dispose() { + aiWriterCubit.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: aiWriterCubit, + child: NotificationListener( + onNotification: handleScrollNotification, + child: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state is DocumentContentEmptyAiWriterState) { + showConfirmDialog( + context: context, + title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: LocaleKeys + .ai_continueWritingEmptyDocumentDescription + .tr(), + onConfirm: state.onConfirm, + ); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous is GeneratingAiWriterState && + current is ReadyAiWriterState, + listener: (context, state) { + widget.editorState.updateSelectionWithReason(null); + }, + ), + ], + child: OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return BlocBuilder( + builder: (context, state) { + return AiWriterGestureDetector( + behavior: state is RegisteredAiWriter + ? HitTestBehavior.translucent + : HitTestBehavior.deferToChild, + onPointerEvent: () => onTapOutside(context), + ); + }, + ); + }, + child: widget.child, + ), + ), + ), + ); + } + + bool handleScrollNotification(ScrollNotification notification) { + if (!aiWriterRegistered) { + return false; + } + + if (notification is UserScrollNotification) { + debounceResetUserHasScrolled(); + userHasScrolled = true; + throttler.cancel(); + } + + return false; + } + + void debounceResetUserHasScrolled() { + Debounce.debounce( + 'user_has_scrolled', + const Duration(seconds: 3), + () => userHasScrolled = false, + ); + } + + void onTapOutside(BuildContext context) { + final aiWriterCubit = context.read(); + + if (aiWriterCubit.hasUnusedResponse()) { + showConfirmDialog( + context: context, + title: LocaleKeys.button_discard.tr(), + description: LocaleKeys.document_plugins_discardResponse.tr(), + confirmLabel: LocaleKeys.button_discard.tr(), + style: ConfirmPopupStyle.cancelAndOk, + onConfirm: () { + Future(() async { + await aiWriterCubit.stopStream(); + await aiWriterCubit.exit(); + }); + }, + onCancel: () {}, + ); + } else { + Future(() async { + await aiWriterCubit.stopStream(); + await aiWriterCubit.exit(); + }); + } + } + + bool cancelShortcutHandler(KeyEvent event) { + if (event is! KeyUpEvent) { + return false; + } + + switch (event.logicalKey) { + case LogicalKeyboardKey.escape: + if (aiWriterCubit.state case GeneratingAiWriterState _) { + aiWriterCubit.stopStream(); + } else if (aiWriterCubit.hasUnusedResponse()) { + showConfirmDialog( + context: context, + title: LocaleKeys.button_discard.tr(), + description: LocaleKeys.document_plugins_discardResponse.tr(), + confirmLabel: LocaleKeys.button_discard.tr(), + style: ConfirmPopupStyle.cancelAndOk, + onConfirm: () { + Future(() async { + await aiWriterCubit.stopStream(); + await aiWriterCubit.exit(); + }); + }, + onCancel: () {}, + ); + } else { + Future(() async { + await aiWriterCubit.stopStream(); + await aiWriterCubit.exit(); + }); + } + return true; + case LogicalKeyboardKey.keyC + when HardwareKeyboard.instance.logicalKeysPressed + .contains(LogicalKeyboardKey.controlLeft): + if (aiWriterCubit.state case GeneratingAiWriterState _) { + aiWriterCubit.stopStream(); + } + return true; + default: + break; + } + + return false; + } + + void onAppendToDocument() { + if (!aiWriterRegistered || userHasScrolled) { + return; + } + + throttler.call(() { + if (aiWriterCubit.aiWriterNode != null) { + final path = aiWriterCubit.aiWriterNode!.path; + widget.editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: path)), + ); + } + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 58c4deb1b1..1904b10934 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -70,6 +70,7 @@ class MarkdownTextRobot { /// The text will be inserted into the document but only in memory Future appendMarkdownText( String text, { + bool updateSelection = true, Map? attributes, }) async { _markdownText += text; @@ -77,6 +78,7 @@ class MarkdownTextRobot { await _lock.synchronized(() async { await _refresh( inMemoryUpdate: true, + updateSelection: updateSelection, attributes: attributes, ); }); @@ -95,7 +97,6 @@ class MarkdownTextRobot { await _lock.synchronized(() async { await _refresh( inMemoryUpdate: true, - updateSelection: false, attributes: attributes, ); }); @@ -155,7 +156,7 @@ class MarkdownTextRobot { Future _refresh({ required bool inMemoryUpdate, - bool updateSelection = true, + bool updateSelection = false, Map? attributes, }) async { final position = _insertPosition; @@ -203,10 +204,6 @@ class MarkdownTextRobot { offset: lastDelta.length, ), ); - - if (!updateSelection) { - insertTransaction.afterSelection = null; - } } await editorState.apply( @@ -215,6 +212,7 @@ class MarkdownTextRobot { inMemoryUpdate: inMemoryUpdate, recordUndo: !inMemoryUpdate, ), + withUpdateSelection: updateSelection, ); _insertedNodes = newNodes; diff --git a/frontend/appflowy_flutter/lib/util/throttle.dart b/frontend/appflowy_flutter/lib/util/throttle.dart index c8c6dcf0ca..0aaa9f2d3a 100644 --- a/frontend/appflowy_flutter/lib/util/throttle.dart +++ b/frontend/appflowy_flutter/lib/util/throttle.dart @@ -16,6 +16,10 @@ class Throttler { }); } + void cancel() { + _timer?.cancel(); + } + void dispose() { _timer?.cancel(); _timer = null; diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index aab5de8169..2abd4ffb97 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -162,20 +162,22 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; - final command = AiWriterCommand.explain; - final node = aiWriterNode( - command: command, - selection: selection, - ); return AiWriterCubit( documentId: '', - getAiWriterNode: () => node, editorState: editorState, - initialCommand: command, aiService: _MockAIRepository(), ); }, - act: (bloc) => bloc.init(), + act: (bloc) => bloc.register( + aiWriterNode( + command: AiWriterCommand.explain, + selection: Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ), + ), + ), + wait: Duration(seconds: 1), expect: () => [ isA() .having((s) => s.markdownText, 'result', isEmpty), @@ -216,19 +218,22 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; - final node = aiWriterNode( - command: AiWriterCommand.explain, - selection: selection, - ); return AiWriterCubit( documentId: '', - getAiWriterNode: () => node, editorState: editorState, - initialCommand: AiWriterCommand.explain, aiService: _MockErrorRepository(), ); }, - act: (bloc) => bloc.init(), + act: (bloc) => bloc.register( + aiWriterNode( + command: AiWriterCommand.explain, + selection: Selection( + start: Position(path: [0]), + end: Position(path: [2], offset: text3.length), + ), + ), + ), + wait: Duration(seconds: 1), expect: () => [ isA() .having((s) => s.markdownText, 'result', isEmpty), @@ -264,12 +269,10 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', - getAiWriterNode: () => aiNode, editorState: editorState, - initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepository(), ); - bloc.init(); + bloc.register(aiNode); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); @@ -314,12 +317,10 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', - getAiWriterNode: () => aiNode, editorState: editorState, - initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepository(), ); - bloc.init(); + bloc.register(aiNode); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.discard); await blocResponseFuture(); @@ -355,12 +356,10 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', - getAiWriterNode: () => aiNode, editorState: editorState, - initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepositoryLess(), ); - bloc.init(); + bloc.register(aiNode); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); @@ -394,12 +393,10 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', - getAiWriterNode: () => aiNode, editorState: editorState, - initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepositoryMore(), ); - bloc.init(); + bloc.register(aiNode); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); From aacd795ae040ef476837375f712e93a115084b7d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:33:51 +0800 Subject: [PATCH 181/384] chore: ai writer more actions (#7576) * feat: add more button to ai writer input box * chore: adjust button padding * chore: adjust icon color --- .../desktop_prompt_text_field.dart | 12 + .../prompt_input/select_sources_menu.dart | 4 +- .../ai/ai_writer_block_component.dart | 264 ++++++------------ .../operations/ai_writer_node_extension.dart | 2 +- .../ai/suggestion_action_bar.dart | 63 ----- .../ai_writer_prompt_input_more_button.dart | 175 ++++++++++++ .../widgets/ai_writer_suggestion_actions.dart | 110 ++++++++ frontend/resources/translations/en.json | 3 +- 8 files changed, 394 insertions(+), 239 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index de641d479c..c446e30f60 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -22,6 +22,7 @@ class DesktopPromptInput extends StatefulWidget { required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, this.hideDecoration = false, + this.extraBottomActionButton, }); final bool isStreaming; @@ -31,6 +32,7 @@ class DesktopPromptInput extends StatefulWidget { final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; final bool hideDecoration; + final Widget? extraBottomActionButton; @override State createState() => _DesktopPromptInputState(); @@ -178,6 +180,8 @@ class _DesktopPromptInputState extends State { widget.selectedSourcesNotifier, onUpdateSelectedSources: widget.onUpdateSelectedSources, + extraBottomActionButton: + widget.extraBottomActionButton, ), ), ), @@ -565,6 +569,7 @@ class _PromptBottomActions extends StatelessWidget { required this.onStopStreaming, required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, + this.extraBottomActionButton, }); final bool showPredefinedFormats; @@ -575,6 +580,7 @@ class _PromptBottomActions extends StatelessWidget { final void Function() onStopStreaming; final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; + final Widget? extraBottomActionButton; @override Widget build(BuildContext context) { @@ -599,6 +605,12 @@ class _PromptBottomActions extends StatelessWidget { DesktopAIChatSizes.inputActionBarButtonSpacing, ), ], + if (extraBottomActionButton != null) ...[ + extraBottomActionButton!, + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + ], // _mentionButton(context), // const HSpace( // DesktopAIPromptSizes.actionBarButtonSpacing, diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart index d7c920c49c..51357e6a0b 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart @@ -145,7 +145,7 @@ class _IndicatorButton extends StatelessWidget { children: [ FlowySvg( FlowySvgs.ai_page_s, - color: Theme.of(context).iconTheme.color, + color: Theme.of(context).hintColor, ), const HSpace(2.0), ValueListenableBuilder( @@ -170,7 +170,7 @@ class _IndicatorButton extends StatelessWidget { FlowySvg( FlowySvgs.ai_source_drop_down_s, color: Theme.of(context).hintColor, - size: const Size.square(10), + size: const Size.square(8), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index c1b533ccca..1e0bac83c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -6,7 +6,6 @@ import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,7 +14,8 @@ import 'package:universal_platform/universal_platform.dart'; import 'operations/ai_writer_cubit.dart'; import 'operations/ai_writer_entities.dart'; import 'operations/ai_writer_node_extension.dart'; -import 'suggestion_action_bar.dart'; +import 'widgets/ai_writer_suggestion_actions.dart'; +import 'widgets/ai_writer_prompt_input_more_button.dart'; class AiWriterBlockKeys { const AiWriterBlockKeys._(); @@ -169,7 +169,7 @@ class _AIWriterBlockComponentState extends State { } } -class OverlayContent extends StatelessWidget { +class OverlayContent extends StatefulWidget { const OverlayContent({ super.key, required this.editorState, @@ -179,6 +179,19 @@ class OverlayContent extends StatelessWidget { final EditorState editorState; final Node node; + @override + State createState() => _OverlayContentState(); +} + +class _OverlayContentState extends State { + final showCommandsToggle = ValueNotifier(false); + + @override + void dispose() { + showCommandsToggle.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -186,10 +199,11 @@ class OverlayContent extends StatelessWidget { if (state is IdleAiWriterState) { return const SizedBox.shrink(); } - final selection = node.aiWriterSelection; + final selection = widget.node.aiWriterSelection; final showSuggestionPopup = state is ReadyAiWriterState && !state.isFirstRun; - final showActionPopup = state is ReadyAiWriterState && state.isFirstRun; + final isInitialReadyState = + state is ReadyAiWriterState && state.isFirstRun; final markdownText = switch (state) { final ReadyAiWriterState ready => ready.markdownText, final GeneratingAiWriterState generating => generating.markdownText, @@ -200,10 +214,6 @@ class OverlayContent extends StatelessWidget { final isLightMode = Theme.of(context).isLightMode; final darkBorderColor = isLightMode ? Color(0x1F1F2329) : Color(0xFF505469); - final lightBorderColor = - Theme.of(context).brightness == Brightness.light - ? ColorSchemeConstants.lightBorderColor - : ColorSchemeConstants.darkBorderColor; return Column( mainAxisSize: MainAxisSize.min, @@ -220,10 +230,8 @@ class OverlayContent extends StatelessWidget { borderColor: darkBorderColor, ), child: SuggestionActionBar( - actions: _getSuggestedActions( - currentCommand: state.command, - hasSelection: hasSelection, - ), + currentCommand: state.command, + hasSelection: hasSelection, onTap: (action) { _onSelectSuggestionAction(context, action); }, @@ -288,10 +296,8 @@ class OverlayContent extends StatelessWidget { if (showSuggestionPopup) ...[ const VSpace(4.0), SuggestionActionBar( - actions: _getSuggestedActions( - currentCommand: state.command, - hasSelection: hasSelection, - ), + currentCommand: state.command, + hasSelection: hasSelection, onTap: (action) { _onSelectSuggestionAction(context, action); }, @@ -310,16 +316,43 @@ class OverlayContent extends StatelessWidget { decoration: markdownText.isNotEmpty ? _getInputChildDecoration(context) : _getSingleChildDeocoration(context), - child: MainContentArea(), + child: MainContentArea( + isDocumentEmpty: _isDocumentEmpty(), + isInitialReadyState: isInitialReadyState, + showCommandsToggle: showCommandsToggle, + ), ), ], ), ), - ..._bottomActions( - context, - showActionPopup, - hasSelection, - lightBorderColor, + ValueListenableBuilder( + valueListenable: showCommandsToggle, + builder: (context, value, child) { + if (!value || !isInitialReadyState) { + return const SizedBox.shrink(); + } + return Align( + alignment: AlignmentDirectional.centerEnd, + child: BottomCommandButtons( + hasSelection: hasSelection, + editorState: widget.editorState, + onSelectCommand: (command) { + final promptInputBloc = context.read(); + final showPredefinedFormats = + promptInputBloc.state.showPredefinedFormats; + final predefinedFormat = + promptInputBloc.state.predefinedFormat; + + context.read().runCommand( + command, + predefinedFormat: + showPredefinedFormats ? predefinedFormat : null, + isFirstRun: true, + ); + }, + ), + ); + }, ), ], ); @@ -327,41 +360,6 @@ class OverlayContent extends StatelessWidget { ); } - Widget _bottomButton(AiWriterCommand command) { - return Builder( - builder: (context) { - return SizedBox( - height: 30.0, - child: FlowyButton( - leftIcon: FlowySvg( - command.icon, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), - margin: const EdgeInsets.all(6.0), - text: FlowyText( - command.i18n, - figmaLineHeight: 20, - ), - onTap: () { - final aiInputBloc = context.read(); - final showPredefinedFormats = - aiInputBloc.state.showPredefinedFormats; - final predefinedFormat = aiInputBloc.state.predefinedFormat; - - context.read().runCommand( - command, - predefinedFormat: - showPredefinedFormats ? predefinedFormat : null, - isFirstRun: false, - ); - }, - ), - ); - }, - ); - } - BoxDecoration _getModalDecoration( BuildContext context, { required Color? color, @@ -406,119 +404,6 @@ class OverlayContent extends StatelessWidget { ); } - List _bottomActions( - BuildContext context, - bool showActionPopup, - bool hasSelection, - Color borderColor, - ) { - if (!showActionPopup) { - return []; - } - - if (editorState.isEmptyForContinueWriting()) { - final documentContext = editorState.document.root.context; - if (documentContext == null) { - return []; - } - final view = documentContext.read().state.view; - if (view.name.isEmpty) { - return []; - } - } - - return [ - // add one here to take into account the border of the main message box. - // It is configured to be on the outside to hide some graphical - // artifacts. - const VSpace(4.0 + 1.0), - Container( - padding: EdgeInsets.all(8.0), - constraints: BoxConstraints(minWidth: 240.0), - decoration: _getModalDecoration( - context, - color: Theme.of(context).colorScheme.surface, - borderColor: borderColor, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - child: IntrinsicWidth( - child: SeparatedColumn( - separatorBuilder: () => const VSpace(4.0), - crossAxisAlignment: CrossAxisAlignment.start, - children: _getCommands( - hasSelection: hasSelection, - ), - ), - ), - ), - ]; - } - - List _getCommands({required bool hasSelection}) { - if (hasSelection) { - return [ - _bottomButton(AiWriterCommand.improveWriting), - _bottomButton(AiWriterCommand.fixSpellingAndGrammar), - _bottomButton(AiWriterCommand.explain), - const Divider(height: 1.0, thickness: 1.0), - _bottomButton(AiWriterCommand.makeLonger), - _bottomButton(AiWriterCommand.makeShorter), - ]; - } else { - return [ - _bottomButton(AiWriterCommand.continueWriting), - ]; - } - } - - List _getSuggestedActions({ - required AiWriterCommand currentCommand, - required bool hasSelection, - }) { - if (hasSelection) { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - AiWriterCommand.fixSpellingAndGrammar || - AiWriterCommand.improveWriting || - AiWriterCommand.makeShorter || - AiWriterCommand.makeLonger => - [ - SuggestionAction.accept, - SuggestionAction.discard, - SuggestionAction.insertBelow, - SuggestionAction.rewrite, - ], - }; - } else { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - _ => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - }; - } - } - void _onSelectSuggestionAction( BuildContext context, SuggestionAction action, @@ -530,10 +415,33 @@ class OverlayContent extends StatelessWidget { predefinedFormat, ); } + + bool _isDocumentEmpty() { + if (widget.editorState.isEmptyForContinueWriting()) { + final documentContext = widget.editorState.document.root.context; + if (documentContext == null) { + return true; + } + final view = documentContext.read().state.view; + if (view.name.isEmpty) { + return true; + } + } + return false; + } } class MainContentArea extends StatelessWidget { - const MainContentArea({super.key}); + const MainContentArea({ + super.key, + required this.isInitialReadyState, + required this.isDocumentEmpty, + required this.showCommandsToggle, + }); + + final bool isInitialReadyState; + final bool isDocumentEmpty; + final ValueNotifier showCommandsToggle; @override Widget build(BuildContext context) { @@ -553,6 +461,18 @@ class MainContentArea extends StatelessWidget { ...sources, ]; }, + extraBottomActionButton: isInitialReadyState + ? ValueListenableBuilder( + valueListenable: showCommandsToggle, + builder: (context, value, _) { + return AiWriterPromptMoreButton( + isEnabled: !isDocumentEmpty, + isSelected: value, + onTap: () => showCommandsToggle.value = !value, + ); + }, + ) + : null, ); } if (state is GeneratingAiWriterState) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 1119917e62..168854a34d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -104,7 +104,7 @@ extension AiWriterNodeExtension on EditorState { start: Position(path: [0]), end: selection?.normalized.end ?? this.selection?.normalized.end ?? - Position(path: [0]), + Position(path: getLastSelectable()?.$1.path ?? [0]), ); // if the selected nodes are not entirely selected, slice the nodes diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart deleted file mode 100644 index a16fc44641..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'operations/ai_writer_entities.dart'; - -class SuggestionActionBar extends StatelessWidget { - const SuggestionActionBar({ - super.key, - required this.actions, - required this.onTap, - }); - - final List actions; - final void Function(SuggestionAction) onTap; - - @override - Widget build(BuildContext context) { - return SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(4.0), - children: actions - .map( - (action) => SuggestionActionButton( - action: action, - onTap: () => onTap(action), - ), - ) - .toList(), - ); - } -} - -class SuggestionActionButton extends StatelessWidget { - const SuggestionActionButton({ - super.key, - required this.action, - required this.onTap, - }); - - final SuggestionAction action; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: FlowyButton( - text: FlowyText( - action.i18n, - figmaLineHeight: 20, - ), - leftIcon: action.buildIcon(context), - iconPadding: 4.0, - margin: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 4.0, - ), - onTap: onTap, - useIntrinsicWidth: true, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart new file mode 100644 index 0000000000..1e2641d796 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart @@ -0,0 +1,175 @@ +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/colorscheme/default_colorscheme.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 '../operations/ai_writer_entities.dart'; + +class AiWriterPromptMoreButton extends StatelessWidget { + const AiWriterPromptMoreButton({ + super.key, + required this.isEnabled, + required this.isSelected, + required this.onTap, + }); + + final bool isEnabled; + final bool isSelected; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: !isEnabled, + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: DesktopAIPromptSizes.actionBarButtonSize, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + isSelected: () => isSelected, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.ai_sparks_s, + color: isEnabled + ? Theme.of(context).hintColor + : Theme.of(context).disabledColor, + ), + const HSpace(2.0), + FlowyText( + LocaleKeys.ai_more.tr(), + fontSize: 12, + figmaLineHeight: 16, + color: isEnabled + ? Theme.of(context).hintColor + : Theme.of(context).disabledColor, + ), + const HSpace(2.0), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class BottomCommandButtons extends StatelessWidget { + const BottomCommandButtons({ + super.key, + required this.hasSelection, + required this.editorState, + required this.onSelectCommand, + }); + + final EditorState editorState; + final bool hasSelection; + final void Function(AiWriterCommand) onSelectCommand; + + @override + Widget build(BuildContext context) { + return Container( + // add one here to take into account the border of the main message box. + // It is configured to be on the outside to hide some graphical + // artifacts. + margin: EdgeInsets.only(top: 4.0 + 1.0), + padding: EdgeInsets.all(8.0), + constraints: BoxConstraints(minWidth: 240.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border.all( + color: Theme.of(context).brightness == Brightness.light + ? ColorSchemeConstants.lightBorderColor + : ColorSchemeConstants.darkBorderColor, + strokeAlign: BorderSide.strokeAlignOutside, + ), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + boxShadow: const [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 20, + color: Color(0x1A1F2329), + ), + ], + ), + child: IntrinsicWidth( + child: Column( + spacing: 4.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: _getCommands( + hasSelection: hasSelection, + ), + ), + ), + ); + } + + List _getCommands({required bool hasSelection}) { + if (hasSelection) { + return [ + _bottomButton(AiWriterCommand.improveWriting), + _bottomButton(AiWriterCommand.fixSpellingAndGrammar), + _bottomButton(AiWriterCommand.explain), + const Divider(height: 1.0, thickness: 1.0), + _bottomButton(AiWriterCommand.makeLonger), + _bottomButton(AiWriterCommand.makeShorter), + ]; + } else { + return [ + _bottomButton(AiWriterCommand.continueWriting), + ]; + } + } + + Widget _bottomButton(AiWriterCommand command) { + return Builder( + builder: (context) { + return FlowyButton( + leftIcon: FlowySvg( + command.icon, + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, + ), + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + text: FlowyText( + command.i18n, + figmaLineHeight: 20, + ), + onTap: () { + final aiInputBloc = context.read(); + final showPredefinedFormats = + aiInputBloc.state.showPredefinedFormats; + final predefinedFormat = aiInputBloc.state.predefinedFormat; + + context.read().runCommand( + command, + predefinedFormat: + showPredefinedFormats ? predefinedFormat : null, + isFirstRun: false, + ); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart new file mode 100644 index 0000000000..d39ede2608 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart @@ -0,0 +1,110 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import '../operations/ai_writer_entities.dart'; + +class SuggestionActionBar extends StatelessWidget { + const SuggestionActionBar({ + super.key, + required this.currentCommand, + required this.hasSelection, + required this.onTap, + }); + + final AiWriterCommand currentCommand; + final bool hasSelection; + final void Function(SuggestionAction) onTap; + + @override + Widget build(BuildContext context) { + return SeparatedRow( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const HSpace(4.0), + children: _getSuggestedActions() + .map( + (action) => SuggestionActionButton( + action: action, + onTap: () => onTap(action), + ), + ) + .toList(), + ); + } + + List _getSuggestedActions() { + if (hasSelection) { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + AiWriterCommand.fixSpellingAndGrammar || + AiWriterCommand.improveWriting || + AiWriterCommand.makeShorter || + AiWriterCommand.makeLonger => + [ + SuggestionAction.accept, + SuggestionAction.discard, + SuggestionAction.insertBelow, + SuggestionAction.rewrite, + ], + }; + } else { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + _ => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + }; + } + } +} + +class SuggestionActionButton extends StatelessWidget { + const SuggestionActionButton({ + super.key, + required this.action, + required this.onTap, + }); + + final SuggestionAction action; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: FlowyButton( + text: FlowyText( + action.i18n, + figmaLineHeight: 20, + ), + leftIcon: action.buildIcon(context), + iconPadding: 4.0, + margin: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 4.0, + ), + onTap: onTap, + useIntrinsicWidth: true, + ), + ); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 55cc15d3ac..7d8cba8b13 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -3160,7 +3160,8 @@ "editing": "Editing", "analyzing": "Analyzing", "continueWritingEmptyDocumentTitle": "Continue writing error", - "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!" + "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!", + "more": "More" }, "autoUpdate": { "criticalUpdateTitle": "Update required to continue", From 10f19069c6877691fb50a9179de8d61f00d61ffa Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:30:42 +0800 Subject: [PATCH 182/384] chore: implement ui --- .../lib/ai/service/appflowy_ai_service.dart | 44 +++++------ .../ai/operations/ai_writer_cubit.dart | 77 +++++++++++++++---- .../ai_writer_test/ai_writer_bloc_test.dart | 14 ++-- .../flowy-server/src/af_cloud/impls/chat.rs | 1 + 4 files changed, 93 insertions(+), 43 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index e3093b6bbf..18b1c71029 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -24,7 +24,8 @@ abstract class AIRepository { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }); @@ -40,18 +41,17 @@ class AppFlowyAIService implements AIRepository { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { final stream = AppFlowyCompletionStream( onStart: onStart, - onProcess: onProcess, + processMessage: processMessage, + processAssistMessage: processAssistMessage, + processError: onError, onEnd: onEnd, - onError: onError, - onComment: (String text) async { - Log.info('Comment: $text'); - }, ); final records = history.map((record) => record.toPB()).toList(); @@ -82,26 +82,26 @@ class AppFlowyAIService implements AIRepository { abstract class CompletionStream { CompletionStream({ required this.onStart, - required this.onProcess, - required this.onComment, + required this.processMessage, + required this.processAssistMessage, + required this.processError, required this.onEnd, - required this.onError, }); final Future Function() onStart; - final Future Function(String text) onProcess; - final Future Function(String text) onComment; + final Future Function(String text) processMessage; + final Future Function(String text) processAssistMessage; + final void Function(AIError error) processError; final Future Function() onEnd; - final void Function(AIError error) onError; } class AppFlowyCompletionStream extends CompletionStream { AppFlowyCompletionStream({ required super.onStart, - required super.onProcess, - required super.onComment, + required super.processMessage, + required super.processAssistMessage, + required super.processError, required super.onEnd, - required super.onError, }) { _startListening(); } @@ -116,7 +116,7 @@ class AppFlowyCompletionStream extends CompletionStream { _subscription = _controller.stream.listen( (event) async { if (event == "AI_RESPONSE_LIMIT") { - onError( + processError( AIError( message: LocaleKeys.ai_textLimitReachedDescription.tr(), code: AIErrorCode.aiResponseLimitExceeded, @@ -125,7 +125,7 @@ class AppFlowyCompletionStream extends CompletionStream { } if (event == "AI_IMAGE_RESPONSE_LIMIT") { - onError( + processError( AIError( message: LocaleKeys.ai_imageLimitReachedDescription.tr(), code: AIErrorCode.aiImageResponseLimitExceeded, @@ -135,7 +135,7 @@ class AppFlowyCompletionStream extends CompletionStream { if (event.startsWith("AI_MAX_REQUIRED:")) { final msg = event.substring(16); - onError( + processError( AIError( message: msg, code: AIErrorCode.other, @@ -148,11 +148,11 @@ class AppFlowyCompletionStream extends CompletionStream { } if (event.startsWith("data:")) { - await onProcess(event.substring(5)); + await processMessage(event.substring(5)); } if (event.startsWith("comment:")) { - await onComment(event.substring(8)); + await processAssistMessage(event.substring(8)); } if (event.startsWith("finish:")) { @@ -160,7 +160,7 @@ class AppFlowyCompletionStream extends CompletionStream { } if (event.startsWith("error:")) { - onError( + processError( AIError(message: event.substring(6), code: AIErrorCode.other), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 133b0b5562..3521c2c63d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -293,7 +293,7 @@ class AiWriterCubit extends Cubit { AiWriterRecord.user(content: prompt), ); }, - onProcess: (text) async { + processMessage: (text) async { await _textRobot.appendMarkdownText( text, updateSelection: false, @@ -301,14 +301,33 @@ class AiWriterCubit extends Cubit { ); onAppendToDocument?.call(); }, + processAssistMessage: (text) async { + if (state case final GeneratingAiWriterState generatingState) { + emit( + GeneratingAiWriterState( + command, + taskId: generatingState.taskId, + markdownText: generatingState.markdownText + text, + ), + ); + } + }, onEnd: () async { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit(ReadyAiWriterState(command, isFirstRun: false)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); + if (state case final GeneratingAiWriterState generatingState) { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit( + ReadyAiWriterState( + command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); + records.add( + AiWriterRecord.ai(content: _textRobot.markdownText), + ); + } }, onError: (error) async { emit(ErrorAiWriterState(command, error: error)); @@ -392,7 +411,7 @@ class AiWriterCubit extends Cubit { ); _textRobot.start(position: position); }, - onProcess: (text) async { + processMessage: (text) async { await _textRobot.appendMarkdownText( text, updateSelection: false, @@ -400,12 +419,29 @@ class AiWriterCubit extends Cubit { ); onAppendToDocument?.call(); }, + processAssistMessage: (text) async { + if (state case final GeneratingAiWriterState generatingState) { + emit( + GeneratingAiWriterState( + command, + taskId: generatingState.taskId, + markdownText: generatingState.markdownText + text, + ), + ); + } + }, onEnd: () async { - if (state case GeneratingAiWriterState _) { + if (state case final GeneratingAiWriterState generatingState) { await _textRobot.stop( attributes: ApplySuggestionFormatType.replace.attributes, ); - emit(ReadyAiWriterState(command, isFirstRun: false)); + emit( + ReadyAiWriterState( + command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); } records.add( AiWriterRecord.ai(content: _textRobot.markdownText), @@ -468,7 +504,7 @@ class AiWriterCubit extends Cubit { ); _textRobot.start(position: position); }, - onProcess: (text) async { + processMessage: (text) async { await _textRobot.appendMarkdownText( text, updateSelection: false, @@ -476,8 +512,19 @@ class AiWriterCubit extends Cubit { ); onAppendToDocument?.call(); }, + processAssistMessage: (text) async { + if (state case final GeneratingAiWriterState generatingState) { + emit( + GeneratingAiWriterState( + command, + taskId: generatingState.taskId, + markdownText: generatingState.markdownText + text, + ), + ); + } + }, onEnd: () async { - if (state is GeneratingAiWriterState) { + if (state case final GeneratingAiWriterState generatingState) { await _textRobot.stop( attributes: ApplySuggestionFormatType.replace.attributes, ); @@ -485,6 +532,7 @@ class AiWriterCubit extends Cubit { ReadyAiWriterState( command, isFirstRun: false, + markdownText: generatingState.markdownText, ), ); records.add( @@ -524,7 +572,7 @@ class AiWriterCubit extends Cubit { completionType: command.toCompletionType(), history: records, onStart: () async {}, - onProcess: (text) async { + processMessage: (text) async { if (state case final GeneratingAiWriterState generatingState) { emit( GeneratingAiWriterState( @@ -535,6 +583,7 @@ class AiWriterCubit extends Cubit { ); } }, + processAssistMessage: (_) async {}, onEnd: () async { if (state case final GeneratingAiWriterState generatingState) { emit( diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index 2abd4ffb97..8b674c0873 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -26,7 +26,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -37,7 +37,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { final lines = text.split('\n'); for (final line in lines) { if (line.isNotEmpty) { - await onProcess('$_aiResponse $line\n\n'); + await processMessage('$_aiResponse $line\n\n'); } } await onEnd(); @@ -57,7 +57,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -66,7 +66,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { Future(() async { await onStart(); // only return 1 line. - await onProcess('Hello World'); + await processMessage('Hello World'); await onEnd(); }), ); @@ -84,7 +84,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -94,7 +94,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { await onStart(); // return 10 lines for (var i = 0; i < 10; i++) { - await onProcess('Hello World\n\n'); + await processMessage('Hello World\n\n'); } await onEnd(); }), @@ -113,7 +113,7 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) onProcess, + required Future Function(String text) processMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 5417f06a65..e4e468f46e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -197,6 +197,7 @@ where .await .map_err(FlowyError::from)? .map_err(FlowyError::from); + Ok(stream.boxed()) } From db2270c8d813ed550cbb74437b1ae9e2d3d4b2cd Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 20 Mar 2025 15:13:14 +0800 Subject: [PATCH 183/384] chore: update local ai commit --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6ec630d09d..a4c37c53f5 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=76d7e7dc885b4d0bd9752aea7ff835de1d48de4e#76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8#1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=76d7e7dc885b4d0bd9752aea7ff835de1d48de4e#76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8#1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fd4b19f6ec..fee2d93f9e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "76d7e7dc885b4d0bd9752aea7ff835de1d48de4e" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" } From 6fd250d4d1e6cc287c87a9b7b279d9560539e984 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 21 Mar 2025 10:04:51 +0800 Subject: [PATCH 184/384] chore: update client api --- frontend/rust-lib/Cargo.lock | 24 ++++++++++++------------ frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a4c37c53f5..530c0a60fd 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "bytes", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "futures-channel", "futures-util", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "async-trait", @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "bincode", "bytes", @@ -2979,7 +2979,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "app-error", "jsonwebtoken", @@ -3609,7 +3609,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "bytes", @@ -6176,7 +6176,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9587c0680f70a58d45c9bb012811bc702df5d213#9587c0680f70a58d45c9bb012811bc702df5d213" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fee2d93f9e..365bd0ed45 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9587c0680f70a58d45c9bb012811bc702df5d213" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9587c0680f70a58d45c9bb012811bc702df5d213" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1d06321789482dd31a44d515eefa06e00871d8ad" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1d06321789482dd31a44d515eefa06e00871d8ad" } [profile.dev] opt-level = 0 From a5c0ad599861bd5ad81f1f87441a8c4cbdc31bde Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 21 Mar 2025 10:07:27 +0800 Subject: [PATCH 185/384] chore: fix test --- frontend/rust-lib/event-integration-test/src/chat_event.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index f12f3d200a..36fdb411a0 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -101,6 +101,7 @@ impl EventIntegrationTest { rag_ids: vec![], format: None, history: vec![], + custom_prompt: None, }; EventBuilder::new(self.clone()) .event(AIEvent::CompleteText) From f1b2f51a06f2d86bf2dac77e8da9fa65f2200f0e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:20:47 +0800 Subject: [PATCH 186/384] chore: fix test --- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++++---------- .../ai_writer_test/ai_writer_bloc_test.dart | 4 ++ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index 8b674c0873..bffe27e985 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -27,6 +27,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -58,6 +59,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -85,6 +87,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { @@ -114,6 +117,7 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required CompletionTypePB completionType, required Future Function() onStart, required Future Function(String text) processMessage, + required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, }) async { From 182101023be878d97acbad009f80eedc693ed97d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:37:10 +0800 Subject: [PATCH 187/384] chore: remove actions in explanation when not explain --- .../editor_plugins/ai/ai_writer_block_component.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 1e0bac83c5..b3e325fedb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -293,7 +293,8 @@ class _OverlayContentState extends State { ), ), ), - if (showSuggestionPopup) ...[ + if (showSuggestionPopup && + state.command == AiWriterCommand.explain) ...[ const VSpace(4.0), SuggestionActionBar( currentCommand: state.command, From c79d014305a5001d34d0ea9592b881faae1ab764 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:49:41 +0800 Subject: [PATCH 188/384] chore: revert podfile changes and update error --- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++++++---------- frontend/rust-lib/flowy-error/src/code.rs | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index dad5f84e38..28ff1a5a20 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -357,7 +357,7 @@ pub enum ErrorCode { #[error("Requested namespace has one or more invalid characters")] CustomNamespaceInvalidCharacter = 122, - #[error("Requested namespace has one or more invalid characters")] + #[error("AI Service is unavailable")] AIServiceUnavailable = 123, #[error("AI Image Response limit exceeded")] From deb019aa4a5bc5907327f986522d77236eaab336 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:14:49 +0800 Subject: [PATCH 189/384] chore: don't allow double register two ai nodes --- .../editor_plugins/ai/operations/ai_writer_cubit.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 3521c2c63d..3b0e04e828 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -51,6 +51,10 @@ class AiWriterCubit extends Cubit { } void register(Node node) async { + if (aiWriterNode != null && node.id != aiWriterNode!.id) { + await removeAiWriterNode(editorState, node); + return; + } aiWriterNode = node; onCreateNode?.call(); From 87015f71332251110a1c92f98b31e7bfe19c609e Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 21 Mar 2025 11:46:58 +0800 Subject: [PATCH 190/384] chore: upgrade lai commit --- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 530c0a60fd..60d65be5cc 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8#1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cd991507870d225e76df41164deee356cd9921a7#cd991507870d225e76df41164deee356cd9921a7" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8#1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cd991507870d225e76df41164deee356cd9921a7#cd991507870d225e76df41164deee356cd9921a7" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 365bd0ed45..515bd1d673 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "1d5f16d07506dfc05a9fbca673b94d0a51a4a9f8" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cd991507870d225e76df41164deee356cd9921a7" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cd991507870d225e76df41164deee356cd9921a7" } From 05949d2f87f129b5a77014f1b9b167fc15f7502f Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 23 Mar 2025 21:53:05 +0800 Subject: [PATCH 191/384] chore: support switch ai model in chat or ai writer --- .../lib/ai/service/ai_input_control.dart | 138 ++++++++++++++ .../lib/ai/service/ai_prompt_input_bloc.dart | 74 +++----- .../lib/ai/service/select_model_bloc.dart | 82 +++++++++ .../desktop_prompt_text_field.dart | 170 +++++++++++++++++- .../application/ai_model_switch_listener.dart | 53 ++++++ .../lib/plugins/ai_chat/chat_page.dart | 1 + .../ai/ai_writer_block_component.dart | 4 + .../settings/ai/settings_ai_bloc.dart | 2 +- frontend/resources/translations/en.json | 4 +- frontend/rust-lib/Cargo.lock | 43 ++--- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-codegen/src/ts_event/mod.rs | 4 +- .../event-integration-test/src/chat_event.rs | 27 +-- .../tests/chat/ai_tool_test.rs | 19 -- .../event-integration-test/tests/chat/mod.rs | 1 - frontend/rust-lib/flowy-ai-pub/Cargo.toml | 1 + frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 20 +++ frontend/rust-lib/flowy-ai/src/ai_manager.rs | 168 +++++++++++++++-- frontend/rust-lib/flowy-ai/src/chat.rs | 26 ++- frontend/rust-lib/flowy-ai/src/completion.rs | 26 ++- frontend/rust-lib/flowy-ai/src/entities.rs | 75 +++++++- .../rust-lib/flowy-ai/src/event_handler.rs | 43 +++-- frontend/rust-lib/flowy-ai/src/event_map.rs | 28 ++- frontend/rust-lib/flowy-ai/src/lib.rs | 1 + .../flowy-ai/src/local_ai/controller.rs | 11 ++ .../src/middleware/chat_service_mw.rs | 81 ++++++--- .../rust-lib/flowy-ai/src/notification.rs | 1 + frontend/rust-lib/flowy-ai/src/util.rs | 3 + frontend/rust-lib/flowy-core/Cargo.toml | 16 +- .../src/deps_resolve/cloud_service_impl.rs | 16 +- .../flowy-server/src/af_cloud/impls/chat.rs | 23 ++- .../rust-lib/flowy-server/src/default_impl.rs | 8 +- 32 files changed, 938 insertions(+), 235 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart create mode 100644 frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart delete mode 100644 frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs create mode 100644 frontend/rust-lib/flowy-ai/src/util.rs diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart new file mode 100644 index 0000000000..942c27a3c9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart @@ -0,0 +1,138 @@ +import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:universal_platform/universal_platform.dart'; + +class AIModelStateNotifier { + AIModelStateNotifier({required this.objectId}) + : _isDesktop = UniversalPlatform.isDesktop, + _localAIListener = + UniversalPlatform.isDesktop ? LocalAIStateListener() : null, + _aiModelSwitchListener = AIModelSwitchListener(chatId: objectId); + + final String objectId; + final bool _isDesktop; + final LocalAIStateListener? _localAIListener; + final AIModelSwitchListener _aiModelSwitchListener; + LocalAIPB? _localAIState; + AvailableModelsPB? _availableModels; + + // callbacks + void Function(AiType, bool, String)? onChanged; + void Function(AvailableModelsPB)? onAvailableModelsChanged; + String hintText() { + final aiType = getCurrentAiType(); + if (aiType.isLocal) { + return isEditable() + ? LocaleKeys.chat_inputLocalAIMessageHint.tr() + : LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); + } + return LocaleKeys.chat_inputMessageHint.tr(); + } + + AiType getCurrentAiType() { + // On non-desktop platforms, always return cloud type + if (!_isDesktop) { + return AiType.cloud; + } + + return _availableModels?.selectedModel.isLocal == true + ? AiType.local + : AiType.cloud; + } + + bool isEditable() { + // On non-desktop platforms, always editable (cloud-only) + if (!_isDesktop) { + return true; + } + + return getCurrentAiType().isLocal + ? _localAIState?.state == RunningStatePB.Running + : true; + } + + void _notifyStateChanged() { + onChanged?.call(getCurrentAiType(), isEditable(), hintText()); + } + + Future init() async { + await _loadAvailableModels(); + } + + Future _loadAvailableModels() async { + final payload = AvailableModelsQueryPB(source: objectId); + final result = await AIEventGetAvailableModels(payload).send(); + result.fold( + (models) { + _availableModels = models; + onAvailableModelsChanged?.call(models); + _notifyStateChanged(); + }, + (err) { + Log.error("Failed to get available models: $err"); + }, + ); + } + + void startListening({ + void Function(AiType, bool, String)? onChanged, + void Function(AvailableModelsPB)? onAvailableModelsChanged, + }) { + this.onChanged = onChanged; + this.onAvailableModelsChanged = onAvailableModelsChanged; + + // Only start local AI listener on desktop platforms + if (_isDesktop) { + _localAIListener?.start( + stateCallback: (state) { + _localAIState = state; + + if (state.state == RunningStatePB.Running || + state.state == RunningStatePB.Stopped) { + _loadAvailableModels(); + } + }, + ); + } + + _aiModelSwitchListener.start( + onUpdateSelectedModel: (model) { + if (_availableModels != null) { + final updatedModels = _availableModels!.deepCopy() + ..selectedModel = model; + _availableModels = updatedModels; + onAvailableModelsChanged?.call(updatedModels); + } + + if (model.isLocal && _isDesktop) { + AIEventGetLocalAIState().send().fold( + (localAIState) { + _localAIState = localAIState; + _notifyStateChanged(); + }, + (error) { + Log.error("Failed to get local AI state: $error"); + _notifyStateChanged(); + }, + ); + } else { + _notifyStateChanged(); + } + }, + ); + } + + Future stop() async { + onChanged = null; + await _localAIListener?.stop(); + await _aiModelSwitchListener.stop(); + } +} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index 178265b20a..e2acda9a1e 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -1,14 +1,8 @@ import 'dart:async'; -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/ai/service/ai_input_control.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -18,19 +12,19 @@ part 'ai_prompt_input_bloc.freezed.dart'; class AIPromptInputBloc extends Bloc { AIPromptInputBloc({ + required String objectId, required PredefinedFormat? predefinedFormat, - }) : _listener = LocalAIStateListener(), - super(AIPromptInputState.initial(predefinedFormat)) { + }) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), + super(AIPromptInputState.initial(objectId, predefinedFormat)) { _dispatch(); - _startListening(); _init(); } - final LocalAIStateListener _listener; + final AIModelStateNotifier _aiModelStateNotifier; @override Future close() async { - await _listener.stop(); + await _aiModelStateNotifier.stop(); return super.close(); } @@ -38,29 +32,11 @@ class AIPromptInputBloc extends Bloc { on( (event, emit) { event.when( - updateAIState: (localAIState) { - final aiType = localAIState.enabled ? AiType.local : AiType.cloud; - // final supportChatWithFile = - // aiType.isLocal && localAIState.state == RunningStatePB.Running; - // If local ai is enabled, user can only send messages when the AI is running - final editable = localAIState.enabled - ? localAIState.state == RunningStatePB.Running - : true; - - var hintText = aiType.isLocal - ? LocaleKeys.chat_inputLocalAIMessageHint.tr() - : LocaleKeys.chat_inputMessageHint.tr(); - - if (editable == false && aiType.isLocal) { - hintText = - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); - } - + updateAIState: (aiType, editable, hintText) { emit( state.copyWith( aiType: aiType, supportChatWithFile: false, - localAIState: localAIState, editable: editable, hintText: hintText, ), @@ -127,25 +103,16 @@ class AIPromptInputBloc extends Bloc { ); } - void _startListening() { - _listener.start( - stateCallback: (pluginState) { - if (!isClosed) { - add(AIPromptInputEvent.updateAIState(pluginState)); - } - }, - ); - } - void _init() { - AIEventGetLocalAIState().send().fold( - (localAIState) { + _aiModelStateNotifier.startListening( + onChanged: (aiType, editable, hintText) { if (!isClosed) { - add(AIPromptInputEvent.updateAIState(localAIState)); + add(AIPromptInputEvent.updateAIState(aiType, editable, hintText)); } }, - Log.error, ); + + _aiModelStateNotifier.init(); } Map consumeMetadata() { @@ -164,8 +131,12 @@ class AIPromptInputBloc extends Bloc { @freezed class AIPromptInputEvent with _$AIPromptInputEvent { - const factory AIPromptInputEvent.updateAIState(LocalAIPB localAIState) = - _UpdateAIState; + const factory AIPromptInputEvent.updateAIState( + AiType aiType, + bool editable, + String hintText, + ) = _UpdateAIState; + const factory AIPromptInputEvent.toggleShowPredefinedFormat() = _ToggleShowPredefinedFormat; const factory AIPromptInputEvent.updatePredefinedFormat( @@ -184,24 +155,27 @@ class AIPromptInputEvent with _$AIPromptInputEvent { @freezed class AIPromptInputState with _$AIPromptInputState { const factory AIPromptInputState({ + required String objectId, required AiType aiType, required bool supportChatWithFile, required bool showPredefinedFormats, required PredefinedFormat? predefinedFormat, - required LocalAIPB? localAIState, required List attachedFiles, required List mentionedPages, required bool editable, required String hintText, }) = _AIPromptInputState; - factory AIPromptInputState.initial(PredefinedFormat? format) => + factory AIPromptInputState.initial( + String objectId, + PredefinedFormat? format, + ) => AIPromptInputState( + objectId: objectId, aiType: AiType.cloud, supportChatWithFile: false, showPredefinedFormats: format != null, predefinedFormat: format, - localAIState: null, attachedFiles: [], mentionedPages: [], editable: true, diff --git a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart new file mode 100644 index 0000000000..665533bd40 --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:appflowy/ai/service/ai_input_control.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; + +part 'select_model_bloc.freezed.dart'; + +class SelectModelBloc extends Bloc { + SelectModelBloc({ + required this.objectId, + }) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), + super(const SelectModelState()) { + _aiModelStateNotifier.init(); + _aiModelStateNotifier.startListening( + onAvailableModelsChanged: (models) { + if (!isClosed) { + add(SelectModelEvent.didLoadModels(models)); + } + }, + ); + + on( + (event, emit) async { + await event.when( + selectModel: (AIModelPB model) async { + await AIEventUpdateSelectedModel( + UpdateSelectedModelPB( + source: objectId, + selectedModel: model, + ), + ).send(); + + state.availableModels?.freeze(); + final newAvailableModels = state.availableModels?.rebuild((m) { + m.selectedModel = model; + }); + + emit( + state.copyWith( + availableModels: newAvailableModels, + ), + ); + }, + didLoadModels: (AvailableModelsPB models) { + emit(state.copyWith(availableModels: models)); + }, + ); + }, + ); + } + + final String objectId; + final AIModelStateNotifier _aiModelStateNotifier; + + @override + Future close() async { + await _aiModelStateNotifier.stop(); + await super.close(); + } +} + +@freezed +class SelectModelEvent with _$SelectModelEvent { + const factory SelectModelEvent.selectModel( + AIModelPB model, + ) = _SelectModel; + + const factory SelectModelEvent.didLoadModels( + AvailableModelsPB models, + ) = _DidLoadModels; +} + +@freezed +class SelectModelState with _$SelectModelState { + const factory SelectModelState({ + AvailableModelsPB? availableModels, + }) = _SelectModelState; +} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index c446e30f60..2cd74b5944 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -1,14 +1,17 @@ import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/ai/service/select_model_bloc.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:extended_text_field/extended_text_field.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'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -165,6 +168,7 @@ class _DesktopPromptInputState extends State { top: null, child: TextFieldTapRegion( child: _PromptBottomActions( + objectId: state.objectId, showPredefinedFormats: state.showPredefinedFormats, onTogglePredefinedFormatSection: () => @@ -561,6 +565,7 @@ class PromptInputTextField extends StatelessWidget { class _PromptBottomActions extends StatelessWidget { const _PromptBottomActions({ + required this.objectId, required this.sendButtonState, required this.showPredefinedFormats, required this.onTogglePredefinedFormatSection, @@ -572,6 +577,7 @@ class _PromptBottomActions extends StatelessWidget { this.extraBottomActionButton, }); + final String objectId; final bool showPredefinedFormats; final void Function() onTogglePredefinedFormatSection; final void Function() onStartMention; @@ -589,15 +595,10 @@ class _PromptBottomActions extends StatelessWidget { margin: DesktopAIChatSizes.inputActionBarMargin, child: BlocBuilder( builder: (context, state) { - if (state.localAIState == null) { - return Align( - alignment: AlignmentDirectional.centerEnd, - child: _sendButton(), - ); - } return Row( children: [ _predefinedFormatButton(), + SelectModelButton(objectId: objectId), const Spacer(), if (state.aiType.isCloud) ...[ _selectSourcesButton(context), @@ -683,3 +684,160 @@ class _PromptBottomActions extends StatelessWidget { ); } } + +class SelectModelButton extends StatefulWidget { + const SelectModelButton({ + super.key, + required this.objectId, + }); + + final String objectId; + + @override + State createState() => _SelectModelButtonState(); +} + +class _SelectModelButtonState extends State { + final popoverController = PopoverController(); + late SelectModelBloc bloc; + + @override + void initState() { + super.initState(); + bloc = SelectModelBloc(objectId: widget.objectId); + } + + @override + void dispose() { + popoverController.close(); + bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + // constraints: BoxConstraints.loose(const Size(250, 200)), + offset: const Offset(0.0, -10.0), + direction: PopoverDirection.topWithLeftAligned, + margin: EdgeInsets.zero, + controller: popoverController, + onOpen: () {}, + onClose: () {}, + popupBuilder: (_) { + return BlocProvider.value( + value: bloc, + child: _PopoverSelectModel( + onClose: () => popoverController.close(), + ), + ); + }, + child: _CurrentModelButton( + modelName: state.availableModels?.selectedModel.name ?? "", + onTap: () => popoverController.show(), + ), + ); + }, + ), + ); + } +} + +class _PopoverSelectModel extends StatelessWidget { + const _PopoverSelectModel({ + required this.onClose, + }); + + final VoidCallback onClose; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return ListView.builder( + shrinkWrap: true, + itemCount: state.availableModels?.models.length ?? 0, + padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), + itemBuilder: (context, index) { + return _ModelItem( + model: state.availableModels!.models[index], + onTap: () { + context.read().add( + SelectModelEvent.selectModel( + state.availableModels!.models[index], + ), + ); + onClose(); + }, + ); + }, + ); + }, + ); + } +} + +class _ModelItem extends StatelessWidget { + const _ModelItem({ + required this.model, + required this.onTap, + }); + + final AIModelPB model; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + var modelName = model.name; + if (model.isLocal) { + modelName += " (${LocaleKeys.chat_changeFormat_localModel.tr()})"; + } + return FlowyTextButton( + modelName, + fillColor: Colors.transparent, + onPressed: onTap, + ); + } +} + +class _CurrentModelButton extends StatelessWidget { + const _CurrentModelButton({ + required this.modelName, + required this.onTap, + }); + + final String modelName; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.chat_changeFormat_switchModel.tr(), + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: DesktopAIPromptSizes.actionBarButtonSize, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), + child: FlowyText( + modelName, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart new file mode 100644 index 0000000000..f25ea4deca --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart @@ -0,0 +1,53 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/rust_stream.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +typedef OnUpdateSelectedModel = void Function(AIModelPB model); + +class AIModelSwitchListener { + AIModelSwitchListener({required this.chatId}) { + _parser = ChatNotificationParser(id: chatId, callback: _callback); + _subscription = RustStreamReceiver.listen( + (observable) => _parser?.parse(observable), + ); + } + + final String chatId; + StreamSubscription? _subscription; + ChatNotificationParser? _parser; + + void start({ + OnUpdateSelectedModel? onUpdateSelectedModel, + }) { + this.onUpdateSelectedModel = onUpdateSelectedModel; + } + + OnUpdateSelectedModel? onUpdateSelectedModel; + + void _callback( + ChatNotification ty, + FlowyResult result, + ) { + result.map((r) { + switch (ty) { + case ChatNotification.DidUpdateSelectedModel: + onUpdateSelectedModel?.call(AIModelPB.fromBuffer(r)); + break; + default: + break; + } + }); + } + + Future stop() async { + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index ce84f923d2..a666039ef2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -73,6 +73,7 @@ class AIChatPage extends StatelessWidget { /// [AIPromptInputBloc] is used to handle the user prompt BlocProvider( create: (_) => AIPromptInputBloc( + objectId: view.id, predefinedFormat: PredefinedFormat( imageFormat: ImageFormat.text, textFormat: TextFormat.bulletList, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index b3e325fedb..1d25723854 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -2,6 +2,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -124,9 +125,12 @@ class _AIWriterBlockComponentState extends State { return const SizedBox.shrink(); } + final documentId = context.read()?.documentId; + return BlocProvider( create: (_) => AIPromptInputBloc( predefinedFormat: null, + objectId: documentId ?? editorState.document.root.id, ), child: LayoutBuilder( builder: (context, constraints) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index ec6970ab7b..74a61fe96c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -173,7 +173,7 @@ class SettingsAIBloc extends Bloc { } void _loadModelList() { - AIEventGetAvailableModels().send().then((result) { + AIEventGetServerAvailableModels().send().then((result) { result.fold((config) { if (!isClosed) { add(SettingsAIEvent.didLoadAvailableModels(config.models)); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7d8cba8b13..7c215e83f9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -247,6 +247,8 @@ "table": "Table", "blankDescription": "Format response", "defaultDescription": "Auto mode", + "localModel": "Local Model", + "switchModel": "Switch model", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", @@ -3191,4 +3193,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 60d65be5cc..4e1c67233d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "bytes", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "futures-channel", "futures-util", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "async-trait", @@ -1398,7 +1398,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "bincode", "bytes", @@ -2072,6 +2072,7 @@ dependencies = [ "flowy-error", "futures", "lib-infra", + "serde", "serde_json", ] @@ -2979,7 +2980,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2994,7 +2995,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "app-error", "jsonwebtoken", @@ -3609,7 +3610,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "bytes", @@ -4646,7 +4647,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4666,7 +4667,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4734,19 +4734,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6176,7 +6163,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=1d06321789482dd31a44d515eefa06e00871d8ad#1d06321789482dd31a44d515eefa06e00871d8ad" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 515bd1d673..6b524c41fd 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1d06321789482dd31a44d515eefa06e00871d8ad" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "1d06321789482dd31a44d515eefa06e00871d8ad" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d77dacae32bc25440ca61f675900b80fba6cc9a2" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d77dacae32bc25440ca61f675900b80fba6cc9a2" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs index ff51ff952b..da8c05f360 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs @@ -153,8 +153,8 @@ pub fn parse_event_crate(event_crate: &TsEventCrate) -> Vec { attrs .iter() .filter(|attr| !attr.attrs.event_attrs.ignore) - .enumerate() - .map(|(_index, variant)| EventASTContext::from(&variant.attrs)) + .into_iter() + .map(|variant| EventASTContext::from(&variant.attrs)) .collect::>() }, _ => vec![], diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index 36fdb411a0..c8c638bc85 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -1,8 +1,8 @@ use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; use flowy_ai::entities::{ - ChatMessageListPB, ChatMessageTypePB, CompleteTextPB, CompleteTextTaskPB, CompletionTypePB, - LoadNextChatMessagePB, LoadPrevChatMessagePB, SendChatPayloadPB, + ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB, + SendChatPayloadPB, }; use flowy_ai::event_map::AIEvent; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; @@ -87,27 +87,4 @@ impl EventIntegrationTest { .await .parse::() } - - pub async fn complete_text( - &self, - text: &str, - completion_type: CompletionTypePB, - ) -> CompleteTextTaskPB { - let payload = CompleteTextPB { - text: text.to_string(), - completion_type, - stream_port: 0, - object_id: "".to_string(), - rag_ids: vec![], - format: None, - history: vec![], - custom_prompt: None, - }; - EventBuilder::new(self.clone()) - .event(AIEvent::CompleteText) - .payload(payload) - .async_send() - .await - .parse::() - } } diff --git a/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs deleted file mode 100644 index f8c2f08b50..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs +++ /dev/null @@ -1,19 +0,0 @@ -use event_integration_test::user_event::use_localhost_af_cloud; -use event_integration_test::EventIntegrationTest; -use flowy_ai::entities::CompletionTypePB; - -use std::time::Duration; - -#[tokio::test] -async fn af_cloud_complete_text_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.af_cloud_sign_up().await; - - let _workspace_id = test.get_current_workspace().await.id; - let _task = test - .complete_text("hello world", CompletionTypePB::MakeLonger) - .await; - - tokio::time::sleep(Duration::from_secs(6)).await; -} diff --git a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs index 21c16131e9..773bdab81f 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs @@ -1,2 +1 @@ -mod ai_tool_test; mod chat_message_test; diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml index 3afd04d530..09ca8dc2d4 100644 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-ai-pub/Cargo.toml @@ -12,3 +12,4 @@ client-api = { workspace = true } bytes.workspace = true futures.workspace = true serde_json.workspace = true +serde.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index d0107831ee..4639a93501 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -14,6 +14,7 @@ pub use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; use flowy_error::FlowyError; use futures::stream::BoxStream; use lib_infra::async_trait::async_trait; +use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; @@ -21,6 +22,22 @@ use std::path::Path; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; pub type StreamComplete = BoxStream<'static, Result>; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +pub struct AIModel { + pub name: String, + pub is_local: bool, +} + +impl Default for AIModel { + fn default() -> Self { + Self { + name: "default".to_string(), + is_local: false, + } + } +} + #[async_trait] pub trait ChatCloudService: Send + Sync + 'static { async fn create_chat( @@ -55,6 +72,7 @@ pub trait ChatCloudService: Send + Sync + 'static { chat_id: &str, message_id: i64, format: ResponseFormat, + ai_model: Option, ) -> Result; async fn get_answer( @@ -90,6 +108,7 @@ pub trait ChatCloudService: Send + Sync + 'static { &self, workspace_id: &str, params: CompleteTextParams, + ai_model: Option, ) -> Result; async fn embed_file( @@ -121,4 +140,5 @@ pub trait ChatCloudService: Send + Sync + 'static { ) -> Result<(), FlowyError>; async fn get_available_models(&self, workspace_id: &str) -> Result; + async fn get_workspace_default_model(&self, workspace_id: &str) -> Result; } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 96301a07b0..535a3bf8c8 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -1,7 +1,7 @@ use crate::chat::Chat; use crate::entities::{ - ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, PredefinedFormatPB, - RepeatedRelatedQuestionPB, StreamMessageParams, + AIModelPB, AvailableModelsPB, ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, + FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; use crate::local_ai::controller::LocalAIController; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; @@ -10,12 +10,13 @@ use std::collections::HashMap; use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, ModelList, UpdateChatParams}; +use flowy_ai_pub::cloud::{AIModel, ChatCloudService, ChatSettings, UpdateChatParams}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use crate::notification::{chat_notification_builder, ChatNotification}; +use crate::util::ai_available_models_key; use collab_integrate::persistence::collab_metadata_sql::{ batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata, }; @@ -24,6 +25,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; use std::path::PathBuf; use std::sync::{Arc, Weak}; +use tokio::sync::RwLock; use tracing::{error, info, trace}; pub trait AIUserService: Send + Sync + 'static { @@ -59,7 +61,8 @@ pub struct AIManager { pub external_service: Arc, chats: Arc>>, pub local_ai: Arc, - store_preferences: Arc, + pub store_preferences: Arc, + server_models: Arc>>, } impl AIManager { @@ -99,6 +102,7 @@ impl AIManager { local_ai, external_service, store_preferences, + server_models: Arc::new(Default::default()), } } @@ -114,6 +118,7 @@ impl AIManager { chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), + self.store_preferences.clone(), )) }); if self.local_ai.is_running() { @@ -205,24 +210,25 @@ impl AIManager { save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; let chat = Arc::new(Chat::new( - self.user_service.user_id().unwrap(), + self.user_service.user_id()?, chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), + self.store_preferences.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) } - pub async fn stream_chat_message<'a>( - &'a self, - params: &'a StreamMessageParams<'a>, + pub async fn stream_chat_message( + &self, + params: StreamMessageParams, ) -> Result { - let chat = self.get_or_create_chat_instance(params.chat_id).await?; - let question = chat.stream_chat_message(params).await?; + let chat = self.get_or_create_chat_instance(¶ms.chat_id).await?; + let question = chat.stream_chat_message(¶ms).await?; let _ = self .external_service - .notify_did_send_message(params.chat_id, params.message) + .notify_did_send_message(¶ms.chat_id, ¶ms.message) .await; Ok(question) } @@ -238,19 +244,149 @@ impl AIManager { let question_message_id = chat .get_question_id_from_answer_id(answer_message_id) .await?; + + let preferred_model = self + .store_preferences + .get_object::(&ai_available_models_key(chat_id)); chat - .stream_regenerate_response(question_message_id, answer_stream_port, format) + .stream_regenerate_response( + question_message_id, + answer_stream_port, + format, + preferred_model, + ) .await?; Ok(()) } - pub async fn get_available_models(&self) -> FlowyResult { + pub async fn get_workspace_select_model(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; + let model = self + .cloud_service_wm + .get_workspace_default_model(&workspace_id) + .await?; + Ok(model) + } + + pub async fn get_server_available_models(&self) -> FlowyResult> { + let workspace_id = self.user_service.workspace_id()?; + + // First, try reading from the cache. + { + let cached_models = self.server_models.read().await; + if !cached_models.is_empty() { + return Ok(cached_models.clone()); + } + } + + // Cache miss: fetch from the cloud. let list = self .cloud_service_wm .get_available_models(&workspace_id) .await?; - Ok(list) + let models = list + .models + .into_iter() + .map(|m| m.name) + .collect::>(); + + // Update the cache. + *self.server_models.write().await = models.clone(); + Ok(models) + } + + pub async fn update_selected_model(&self, source: String, model: AIModelPB) -> FlowyResult<()> { + let source_key = ai_available_models_key(&source); + self + .store_preferences + .set_object::(&source_key, &model.clone().into())?; + + chat_notification_builder(&source, ChatNotification::DidUpdateSelectedModel) + .payload(model) + .send(); + Ok(()) + } + + pub async fn get_available_models(&self, source: String) -> FlowyResult { + // Build the models list from server models and mark them as non-local. + let mut models: Vec = self + .get_server_available_models() + .await? + .into_iter() + .map(|name| AIModelPB { + name, + is_local: false, + }) + .collect(); + + // Optionally add the local plugin model. + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + models.push(AIModelPB { + name: local_model, + is_local: true, + }); + } + + if models.is_empty() { + return Ok(AvailableModelsPB { + models, + selected_model: None, + }); + } + + let source_key = ai_available_models_key(&source); + + // Retrieve stored selected model, if any. + let stored_selected = self.store_preferences.get_object::(&source_key); + + // Get workspace default model once. + let workspace_default = self.get_workspace_select_model().await.ok(); + + // Determine the effective selected model. + let effective_selected = stored_selected.unwrap_or_else(|| { + if let Some(ws_name) = workspace_default.clone() { + let model = AIModel { + name: ws_name, + is_local: false, + }; + // Store the default if not present. + let _ = self.store_preferences.set_object(&source_key, &model); + model + } else { + AIModel::default() + } + }); + + // Find a matching model in the available list. + let used_model = models + .iter() + .find(|m| m.name == effective_selected.name) + .cloned() + .or_else(|| { + // If no match, try to use the workspace default if available. + if let Some(ws_name) = workspace_default { + Some(AIModelPB { + name: ws_name, + is_local: false, + }) + } else { + models.first().cloned() + } + }); + + // Update the stored preference if a different model is used. + if let Some(ref used) = used_model { + if used.name != effective_selected.name { + self + .store_preferences + .set_object::(&source_key, &AIModel::from(used.clone()))?; + } + } + + Ok(AvailableModelsPB { + models, + selected_model: used_model, + }) } pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result, FlowyError> { @@ -258,10 +394,11 @@ impl AIManager { match chat { None => { let chat = Arc::new(Chat::new( - self.user_service.user_id().unwrap(), + self.user_service.user_id()?, chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), + self.store_preferences.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) @@ -363,7 +500,6 @@ impl AIManager { pub async fn update_rag_ids(&self, chat_id: &str, rag_ids: Vec) -> FlowyResult<()> { info!("[Chat] update chat:{} rag ids: {:?}", chat_id, rag_ids); - let workspace_id = self.user_service.workspace_id()?; let update_setting = UpdateChatParams { name: None, diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 2ed7a584d0..ea553758df 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -10,11 +10,13 @@ use crate::persistence::{ ChatMessageTable, }; use crate::stream_message::StreamMessage; +use crate::util::ai_available_models_key; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, + AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; @@ -39,6 +41,7 @@ pub struct Chat { latest_message_id: Arc, stop_stream: Arc, stream_buffer: Arc>, + store_preferences: Arc, } impl Chat { @@ -47,6 +50,7 @@ impl Chat { chat_id: String, user_service: Arc, chat_service: Arc, + store_preferences: Arc, ) -> Chat { Chat { uid, @@ -57,6 +61,7 @@ impl Chat { latest_message_id: Default::default(), stop_stream: Arc::new(AtomicBool::new(false)), stream_buffer: Arc::new(Mutex::new(StringBuffer::default())), + store_preferences, } } @@ -81,9 +86,9 @@ impl Chat { } #[instrument(level = "info", skip_all, err)] - pub async fn stream_chat_message<'a>( - &'a self, - params: &'a StreamMessageParams<'a>, + pub async fn stream_chat_message( + &self, + params: &StreamMessageParams, ) -> Result { trace!( "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", @@ -113,7 +118,7 @@ impl Chat { .create_question( &workspace_id, &self.chat_id, - params.message, + ¶ms.message, params.message_type.clone(), &[], ) @@ -138,7 +143,9 @@ impl Chat { // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); - + let preferred_ai_model = self + .store_preferences + .get_object::(&ai_available_models_key(&self.chat_id)); self.stream_response( params.answer_stream_port, answer_stream_buffer, @@ -146,6 +153,7 @@ impl Chat { workspace_id, question.message_id, format, + preferred_ai_model, ); let question_pb = ChatMessagePB::from(question); @@ -158,6 +166,7 @@ impl Chat { question_id: i64, answer_stream_port: i64, format: Option, + ai_model: Option, ) -> FlowyResult<()> { trace!( "[Chat] regenerate and stream chat message: chat_id={}", @@ -183,11 +192,13 @@ impl Chat { workspace_id, question_id, format, + ai_model, ); Ok(()) } + #[allow(clippy::too_many_arguments)] fn stream_response( &self, answer_stream_port: i64, @@ -196,6 +207,7 @@ impl Chat { workspace_id: String, question_id: i64, format: ResponseFormat, + ai_model: Option, ) { let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id.clone(); @@ -204,7 +216,7 @@ impl Chat { tokio::spawn(async move { let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service - .stream_answer(&workspace_id, &chat_id, question_id, format) + .stream_answer(&workspace_id, &chat_id, question_id, format, ai_model) .await { Ok(mut stream) => { diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index fa79308b4f..38dea5f5e4 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -4,14 +4,16 @@ use allo_isolate::Isolate; use dashmap::DashMap; use flowy_ai_pub::cloud::{ - ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionStreamValue, CompletionType, - CustomPrompt, + AIModel, ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionStreamValue, + CompletionType, CustomPrompt, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; +use crate::util::ai_available_models_key; +use flowy_sqlite::kv::KVStorePreferences; use std::sync::{Arc, Weak}; use tokio::select; use tracing::info; @@ -20,17 +22,20 @@ pub struct AICompletion { tasks: Arc>>, cloud_service: Weak, user_service: Weak, + store_preferences: Arc, } impl AICompletion { pub fn new( cloud_service: Weak, user_service: Weak, + store_preferences: Arc, ) -> Self { Self { tasks: Arc::new(DashMap::new()), cloud_service, user_service, + store_preferences, } } @@ -53,7 +58,17 @@ impl AICompletion { .ok_or_else(FlowyError::internal)? .workspace_id()?; let (tx, rx) = tokio::sync::mpsc::channel(1); - let task = CompletionTask::new(workspace_id, complete, self.cloud_service.clone(), rx); + let preferred_model = self + .store_preferences + .get_object::(&ai_available_models_key(&complete.object_id)); + + let task = CompletionTask::new( + workspace_id, + complete, + preferred_model, + self.cloud_service.clone(), + rx, + ); let task_id = task.task_id.clone(); self.tasks.insert(task_id.clone(), tx); @@ -74,12 +89,14 @@ pub struct CompletionTask { stop_rx: tokio::sync::mpsc::Receiver<()>, context: CompleteTextPB, cloud_service: Weak, + preferred_model: Option, } impl CompletionTask { pub fn new( workspace_id: String, context: CompleteTextPB, + preferred_model: Option, cloud_service: Weak, stop_rx: tokio::sync::mpsc::Receiver<()>, ) -> Self { @@ -89,6 +106,7 @@ impl CompletionTask { context, cloud_service, stop_rx, + preferred_model, } } @@ -129,7 +147,7 @@ impl CompletionTask { info!("start completion: {:?}", params); match cloud_service - .stream_complete(&self.workspace_id, params) + .stream_complete(&self.workspace_id, params, self.preferred_model) .await { Ok(mut stream) => loop { diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 70a8b5ff58..355e8d27b5 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -4,8 +4,9 @@ use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; use flowy_ai_pub::cloud::{ - ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, OutputContent, - OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, + AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, + OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, + ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -76,9 +77,9 @@ pub struct StreamChatPayloadPB { } #[derive(Default, Debug)] -pub struct StreamMessageParams<'a> { - pub chat_id: &'a str, - pub message: &'a str, +pub struct StreamMessageParams { + pub chat_id: String, + pub message: String, pub message_type: ChatMessageType, pub answer_stream_port: i64, pub question_stream_port: i64, @@ -182,12 +183,65 @@ pub struct ChatMessageListPB { pub total: i64, } -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct ModelConfigPB { +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct ServerAvailableModelsPB { #[pb(index = 1)] pub models: String, } +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct AvailableModelsQueryPB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub source: String, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct UpdateSelectedModelPB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub source: String, + + #[pb(index = 2)] + pub selected_model: AIModelPB, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct AvailableModelsPB { + #[pb(index = 1)] + pub models: Vec, + + #[pb(index = 2, one_of)] + pub selected_model: Option, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct AIModelPB { + #[pb(index = 1)] + pub name: String, + + #[pb(index = 2)] + pub is_local: bool, +} + +impl From for AIModelPB { + fn from(model: AIModel) -> Self { + Self { + name: model.name, + is_local: model.is_local, + } + } +} + +impl From for AIModel { + fn from(value: AIModelPB) -> Self { + AIModel { + name: value.name, + is_local: value.is_local, + } + } +} + impl From for ChatMessageListPB { fn from(repeated_chat_message: RepeatedChatMessage) -> Self { let messages = repeated_chat_message @@ -471,14 +525,14 @@ pub struct PendingResourcePB { pub enum PendingResourceTypePB { #[default] LocalAIAppRes = 0, - AIModel = 1, + ModelRes = 1, } impl From for PendingResourceTypePB { fn from(value: PendingResource) -> Self { match value { PendingResource::PluginExecutableNotReady { .. } => PendingResourceTypePB::LocalAIAppRes, - _ => PendingResourceTypePB::AIModel, + _ => PendingResourceTypePB::ModelRes, } } } @@ -559,6 +613,9 @@ pub struct UpdateChatSettingsPB { #[pb(index = 2)] pub rag_ids: Vec, + + #[pb(index = 3)] + pub chat_model: String, } #[derive(Debug, Default, Clone, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 5a8018ae4b..1418e84058 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -69,8 +69,8 @@ pub(crate) async fn stream_chat_message_handler( trace!("Stream chat message with metadata: {:?}", metadata); let params = StreamMessageParams { - chat_id: &chat_id, - message: &message, + chat_id, + message, message_type, answer_stream_port, question_stream_port, @@ -79,7 +79,7 @@ pub(crate) async fn stream_chat_message_handler( }; let ai_manager = upgrade_ai_manager(ai_manager)?; - let result = ai_manager.stream_chat_message(¶ms).await?; + let result = ai_manager.stream_chat_message(params).await?; data_result_ok(result) } @@ -103,19 +103,36 @@ pub(crate) async fn regenerate_response_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_model_list_handler( +pub(crate) async fn get_server_model_list_handler( ai_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let available_models = ai_manager.get_available_models().await?; - let models = available_models - .models - .into_iter() - .map(|m| m.name) - .collect::>(); - + let models = ai_manager.get_server_available_models().await?; let models = serde_json::to_string(&json!({"models": models}))?; - data_result_ok(ModelConfigPB { models }) + data_result_ok(ServerAvailableModelsPB { models }) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_chat_models_handler( + data: AFPluginData, + ai_manager: AFPluginState>, +) -> DataResult { + let data = data.try_into_inner()?; + let ai_manager = upgrade_ai_manager(ai_manager)?; + let models = ai_manager.get_available_models(data.source).await?; + data_result_ok(models) +} + +pub(crate) async fn update_selected_model_handler( + data: AFPluginData, + ai_manager: AFPluginState>, +) -> Result<(), FlowyError> { + let data = data.try_into_inner()?; + let ai_manager = upgrade_ai_manager(ai_manager)?; + ai_manager + .update_selected_model(data.source, data.selected_model) + .await?; + Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 7875e7c3c9..84c2266f0b 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -10,9 +10,14 @@ use crate::ai_manager::AIManager; use crate::event_handler::*; pub fn init(ai_manager: Weak) -> AFPlugin { - let user_service = Arc::downgrade(&ai_manager.upgrade().unwrap().user_service); - let cloud_service = Arc::downgrade(&ai_manager.upgrade().unwrap().cloud_service_wm); - let ai_tools = Arc::new(AICompletion::new(cloud_service, user_service)); + let strong_ai_manager = ai_manager.upgrade().unwrap(); + let user_service = Arc::downgrade(&strong_ai_manager.user_service); + let cloud_service = Arc::downgrade(&strong_ai_manager.cloud_service_wm); + let ai_tools = Arc::new(AICompletion::new( + cloud_service, + user_service, + strong_ai_manager.store_preferences.clone(), + )); AFPlugin::new() .name("flowy-ai") .state(ai_manager) @@ -35,12 +40,17 @@ pub fn init(ai_manager: Weak) -> AFPlugin { AIEvent::UpdateLocalAISetting, update_local_ai_setting_handler, ) - .event(AIEvent::GetAvailableModels, get_model_list_handler) + .event( + AIEvent::GetServerAvailableModels, + get_server_model_list_handler, + ) .event(AIEvent::CreateChatContext, create_chat_context_handler) .event(AIEvent::GetChatInfo, create_chat_context_handler) .event(AIEvent::GetChatSettings, get_chat_settings_handler) .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) .event(AIEvent::RegenerateResponse, regenerate_response_handler) + .event(AIEvent::GetAvailableModels, get_chat_models_handler) + .event(AIEvent::UpdateSelectedModel, update_selected_model_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -105,12 +115,18 @@ pub enum AIEvent { #[event(input = "RegenerateResponsePB")] RegenerateResponse = 27, - #[event(output = "ModelConfigPB")] - GetAvailableModels = 28, + #[event(output = "ServerAvailableModelsPB")] + GetServerAvailableModels = 28, #[event(output = "LocalAISettingPB")] GetLocalAISetting = 29, #[event(input = "LocalAISettingPB")] UpdateLocalAISetting = 30, + + #[event(input = "AvailableModelsQueryPB", output = "AvailableModelsPB")] + GetAvailableModels = 31, + + #[event(input = "UpdateSelectedModelPB")] + UpdateSelectedModel = 32, } diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index be6c743d86..884c859660 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -11,3 +11,4 @@ pub mod notification; mod persistence; mod protobuf; mod stream_message; +mod util; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index da24b70145..04d73831b9 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -193,6 +193,10 @@ impl LocalAIController { /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have /// different settings. pub fn is_enabled(&self) -> bool { + if !get_operating_system().is_desktop() { + return false; + } + if let Ok(key) = self .user_service .workspace_id() @@ -204,6 +208,13 @@ impl LocalAIController { } } + pub fn get_plugin_chat_model(&self) -> Option { + if !self.is_enabled() { + return None; + } + Some(self.resource.get_llm_setting().chat_model_name) + } + pub fn open_chat(&self, chat_id: &str) { if !self.is_enabled() { return; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 8e69f14d20..37bf5b5daf 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -9,7 +9,7 @@ use appflowy_plugin::error::PluginError; use std::collections::HashMap; use flowy_ai_pub::cloud::{ - AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, LocalAIConfig, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, @@ -25,7 +25,7 @@ use futures_util::SinkExt; use serde_json::{json, Value}; use std::path::Path; use std::sync::{Arc, Weak}; -use tracing::trace; +use tracing::{info, trace}; pub struct AICloudServiceMiddleware { cloud_service: Arc, @@ -156,12 +156,19 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &str, chat_id: &str, - question_id: i64, + message_id: i64, format: ResponseFormat, + ai_model: Option, ) -> Result { - if self.local_ai.is_enabled() { + let use_local_ai = match &ai_model { + None => false, + Some(model) => model.is_local, + }; + + info!("stream_answer use model: {:?}", ai_model); + if use_local_ai { if self.local_ai.is_running() { - let row = self.get_message_record(question_id)?; + let row = self.get_message_record(message_id)?; match self .local_ai .stream_question(chat_id, &row.content, Some(json!(format)), json!({})) @@ -179,7 +186,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .stream_answer(workspace_id, chat_id, question_id, format) + .stream_answer(workspace_id, chat_id, message_id, format, ai_model) .await } } @@ -273,34 +280,45 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &str, params: CompleteTextParams, + ai_model: Option, ) -> Result { - if self.local_ai.is_running() { - match self - .local_ai - .complete_text_v2( - ¶ms.text, - params.completion_type.unwrap() as u8, - Some(json!(params.format)), - Some(json!(params.metadata)), - ) - .await - { - Ok(stream) => Ok( - CompletionStream::new( - stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), + let use_local_ai = match &ai_model { + None => false, + Some(model) => model.is_local, + }; + + info!("stream_complete use model: {:?}", ai_model); + if use_local_ai { + if self.local_ai.is_running() { + match self + .local_ai + .complete_text_v2( + ¶ms.text, + params.completion_type.unwrap() as u8, + Some(json!(params.format)), + Some(json!(params.metadata)), ) - .map_err(FlowyError::from) - .boxed(), - ), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, + .await + { + Ok(stream) => Ok( + CompletionStream::new( + stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), + ) + .map_err(FlowyError::from) + .boxed(), + ), + Err(err) => { + self.handle_plugin_error(err); + Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) + }, + } + } else { + Err(FlowyError::local_ai_not_ready()) } } else { self .cloud_service - .stream_complete(workspace_id, params) + .stream_complete(workspace_id, params, ai_model) .await } } @@ -364,4 +382,11 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn get_available_models(&self, workspace_id: &str) -> Result { self.cloud_service.get_available_models(workspace_id).await } + + async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + self + .cloud_service + .get_workspace_default_model(workspace_id) + .await + } } diff --git a/frontend/rust-lib/flowy-ai/src/notification.rs b/frontend/rust-lib/flowy-ai/src/notification.rs index 81d1bb92d6..97cc0e9631 100644 --- a/frontend/rust-lib/flowy-ai/src/notification.rs +++ b/frontend/rust-lib/flowy-ai/src/notification.rs @@ -15,6 +15,7 @@ pub enum ChatNotification { UpdateLocalAIState = 6, DidUpdateChatSettings = 7, LocalAIResourceUpdated = 8, + DidUpdateSelectedModel = 9, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-ai/src/util.rs b/frontend/rust-lib/flowy-ai/src/util.rs new file mode 100644 index 0000000000..d1667ae98b --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/util.rs @@ -0,0 +1,3 @@ +pub fn ai_available_models_key(object_id: &str) -> String { + format!("ai_available_models_{}", object_id) +} diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index d1668524cb..7977c33d4f 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -74,14 +74,14 @@ dart = [ "flowy-ai/dart", "flowy-storage/dart", ] -ts = [ - "flowy-user/tauri_ts", - "flowy-folder/tauri_ts", - "flowy-search/tauri_ts", - "flowy-database2/ts", - "flowy-ai/tauri_ts", - "flowy-storage/tauri_ts", -] +#ts = [ +# "flowy-user/tauri_ts", +# "flowy-folder/tauri_ts", +# "flowy-search/tauri_ts", +# "flowy-database2/ts", +# "flowy-ai/tauri_ts", +# "flowy-storage/tauri_ts", +#] openssl_vendored = ["flowy-sqlite/openssl_vendored"] # Enable/Disable AppFlowy Verbose Log Configuration diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 80c22642a4..68adc45c0e 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -21,7 +21,7 @@ use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; @@ -705,13 +705,14 @@ impl ChatCloudService for ServerProvider { chat_id: &str, message_id: i64, format: ResponseFormat, + ai_model: Option, ) -> Result { let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_answer(&workspace_id, &chat_id, message_id, format) + .stream_answer(&workspace_id, &chat_id, message_id, format, ai_model) .await } @@ -772,12 +773,13 @@ impl ChatCloudService for ServerProvider { &self, workspace_id: &str, params: CompleteTextParams, + ai_model: Option, ) -> Result { let workspace_id = workspace_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_complete(&workspace_id, params) + .stream_complete(&workspace_id, params, ai_model) .await } @@ -846,6 +848,14 @@ impl ChatCloudService for ServerProvider { .get_available_models(workspace_id) .await } + + async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + self + .get_server()? + .chat_service() + .get_workspace_default_model(workspace_id) + .await + } } #[async_trait] diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index e4e468f46e..e14f247d88 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -7,8 +7,8 @@ use client_api::entity::chat_dto::{ RepeatedChatMessage, }; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, LocalAIConfig, - ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + LocalAIConfig, ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; @@ -101,12 +101,14 @@ where chat_id: &str, message_id: i64, format: ResponseFormat, + ai_model: Option, ) -> Result { trace!( - "stream_answer: workspace_id={}, chat_id={}, format={:?}", + "stream_answer: workspace_id={}, chat_id={}, format={:?}, model: {:?}", workspace_id, chat_id, - format + format, + ai_model, ); let try_get_client = self.inner.try_get_client(); let result = try_get_client? @@ -117,6 +119,7 @@ where question_id: message_id, format, }, + ai_model.map(|v| v.name), ) .await; @@ -189,11 +192,12 @@ where &self, workspace_id: &str, params: CompleteTextParams, + ai_model: Option, ) -> Result { let stream = self .inner .try_get_client()? - .stream_completion_v2(workspace_id, params) + .stream_completion_v2(workspace_id, params, ai_model.map(|v| v.name)) .await .map_err(FlowyError::from)? .map_err(FlowyError::from); @@ -280,4 +284,13 @@ where .await?; Ok(list) } + + async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + let setting = self + .inner + .try_get_client()? + .get_workspace_settings(workspace_id) + .await?; + Ok(setting.ai_model) + } } diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index d1a16159c4..7dca08754b 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,6 +1,6 @@ use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; @@ -52,6 +52,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { _chat_id: &str, _message_id: i64, _format: ResponseFormat, + _ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } @@ -97,6 +98,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { &self, _workspace_id: &str, _params: CompleteTextParams, + _ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) } @@ -148,4 +150,8 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { async fn get_available_models(&self, _workspace_id: &str) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } + + async fn get_workspace_default_model(&self, _workspace_id: &str) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } } From 44c9d572c813741672026584d6703e523e5d687c Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 24 Mar 2025 09:54:45 +0800 Subject: [PATCH 192/384] fix: using date picker with @ menu with some errors (#7580) --- .../mention/mention_date_block.dart | 9 ------ .../appflowy_date_picker_base.dart | 29 +++++++++---------- .../date_picker/desktop_date_picker.dart | 1 + .../widgets/date_picker_dialog.dart | 14 +++++++-- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index e1df115e15..6c5ee7f527 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -60,8 +60,6 @@ class MentionDateBlock extends StatefulWidget { } class _MentionDateBlockState extends State { - final PopoverMutex mutex = PopoverMutex(); - late bool _includeTime = widget.includeTime; late DateTime? parsedDate = DateTime.tryParse(widget.date); @@ -71,12 +69,6 @@ class _MentionDateBlockState extends State { super.didUpdateWidget(oldWidget); } - @override - void dispose() { - mutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { if (parsedDate == null) { @@ -105,7 +97,6 @@ class _MentionDateBlockState extends State { final options = DatePickerOptions( focusedDay: parsedDate, - popoverMutex: mutex, selectedDay: parsedDate, includeTime: _includeTime, dateFormat: appearance.dateFormat, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart index 8bfc187422..d965670f77 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart @@ -23,6 +23,7 @@ abstract class AppFlowyDatePicker extends StatefulWidget { this.onIncludeTimeChanged, this.onIsRangeChanged, this.onReminderSelected, + this.enableDidUpdate = true, }); final DateTime? dateTime; @@ -55,6 +56,7 @@ abstract class AppFlowyDatePicker extends StatefulWidget { final ReminderOption reminderOption; final OnReminderSelected? onReminderSelected; + final bool enableDidUpdate; } abstract class AppFlowyDatePickerState @@ -75,34 +77,31 @@ abstract class AppFlowyDatePickerState @override void initState() { super.initState(); - - dateTime = widget.dateTime; - startDateTime = widget.isRange ? widget.dateTime : null; - endDateTime = widget.isRange ? widget.endDateTime : null; - includeTime = widget.includeTime; - isRange = widget.isRange; - reminderOption = widget.reminderOption; + initData(); focusedDateTime = widget.dateTime ?? DateTime.now(); } @override void didUpdateWidget(covariant oldWidget) { - dateTime = widget.dateTime; - if (widget.isRange) { - startDateTime = widget.dateTime; - endDateTime = widget.endDateTime; - } else { - startDateTime = endDateTime = null; + if (widget.enableDidUpdate) { + initData(); } - includeTime = widget.includeTime; - isRange = widget.isRange; if (oldWidget.reminderOption != widget.reminderOption) { reminderOption = widget.reminderOption; } super.didUpdateWidget(oldWidget); } + void initData() { + dateTime = widget.dateTime; + startDateTime = widget.isRange ? widget.dateTime : null; + endDateTime = widget.isRange ? widget.endDateTime : null; + includeTime = widget.includeTime; + isRange = widget.isRange; + reminderOption = widget.reminderOption; + } + void onDateSelectedFromDatePicker( DateTime? newStartDateTime, DateTime? newEndDateTime, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart index c404f576b1..fada23e994 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart @@ -32,6 +32,7 @@ class DesktopAppFlowyDatePicker extends AppFlowyDatePicker { super.onIncludeTimeChanged, super.onIsRangeChanged, super.onReminderSelected, + super.enableDidUpdate, this.popoverMutex, this.options = const [], }); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index 301fd038ee..54fc2fac2a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -16,7 +16,6 @@ import 'package:flutter/services.dart'; class DatePickerOptions { DatePickerOptions({ DateTime? focusedDay, - this.popoverMutex, this.selectedDay, this.includeTime = false, this.isRange = false, @@ -31,7 +30,6 @@ class DatePickerOptions { }) : focusedDay = focusedDay ?? DateTime.now(); final DateTime focusedDay; - final PopoverMutex? popoverMutex; final DateTime? selectedDay; final bool includeTime; final bool isRange; @@ -48,6 +46,7 @@ class DatePickerOptions { abstract class DatePickerService { void show(Offset offset, {required DatePickerOptions options}); + void dismiss(); } @@ -60,6 +59,7 @@ class DatePickerMenu extends DatePickerService { final BuildContext context; final EditorState editorState; + PopoverMutex? popoverMutex; OverlayEntry? _menuEntry; @@ -67,6 +67,9 @@ class DatePickerMenu extends DatePickerService { void dismiss() { _menuEntry?.remove(); _menuEntry = null; + popoverMutex?.close(); + popoverMutex?.dispose(); + popoverMutex = null; } @override @@ -97,6 +100,7 @@ class DatePickerMenu extends DatePickerService { } } + popoverMutex = PopoverMutex(); _menuEntry = OverlayEntry( builder: (_) => Material( type: MaterialType.transparency, @@ -119,6 +123,7 @@ class DatePickerMenu extends DatePickerService { offset: Offset(offsetX, offsetY), showBelow: showBelow, options: options, + popoverMutex: popoverMutex, ), ], ), @@ -137,11 +142,13 @@ class _AnimatedDatePicker extends StatelessWidget { required this.offset, required this.showBelow, required this.options, + this.popoverMutex, }); final Offset offset; final bool showBelow; final DatePickerOptions options; + final PopoverMutex? popoverMutex; @override Widget build(BuildContext context) { @@ -165,11 +172,12 @@ class _AnimatedDatePicker extends StatelessWidget { dateFormat: options.dateFormat.simplified, timeFormat: options.timeFormat.simplified, dateTime: options.selectedDay, - popoverMutex: options.popoverMutex, + popoverMutex: popoverMutex, reminderOption: options.selectedReminderOption ?? ReminderOption.none, onDaySelected: options.onDaySelected, onRangeSelected: options.onRangeSelected, onReminderSelected: options.onReminderSelected, + enableDidUpdate: false, ), ), ); From 6f031d0c7e1ab2efee52e41f2ee4de3e41e476b7 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 24 Mar 2025 09:55:23 +0800 Subject: [PATCH 193/384] feat: revamp toolbar link (#7578) * feat: revamp toolbar link * fix: some review issues * chore: add integration test for toolbar link --- .../document/document_toolbar_test.dart | 151 ++++++ .../document/presentation/editor_page.dart | 3 +- .../desktop_floating_toolbar.dart | 18 +- .../link/link_create_menu.dart | 164 +++++++ .../desktop_toolbar/link/link_edit_menu.dart | 429 ++++++++++++++++++ .../desktop_toolbar/link/link_hover_menu.dart | 322 +++++++++++++ .../link/link_search_text_field.dart | 326 +++++++++++++ .../desktop_toolbar/link/link_styles.dart | 34 ++ .../desktop_toolbar/toolbar_cubit.dart | 17 + .../header/emoji_icon_widget.dart | 3 + .../mention/mention_page_block.dart | 12 +- .../custom_link_toolbar_item.dart | 171 ++++++- .../document/presentation/editor_style.dart | 69 ++- frontend/appflowy_flutter/macos/Podfile.lock | 46 +- .../src/flowy_overlay/appflowy_popover.dart | 14 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../flowy_icons/20x/toolbar_link_earth.svg | 3 + .../flowy_icons/20x/toolbar_link_edit.svg | 3 + .../flowy_icons/20x/toolbar_link_unlink.svg | 3 + frontend/resources/translations/en.json | 7 +- 21 files changed, 1750 insertions(+), 51 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart create mode 100644 frontend/resources/flowy_icons/20x/toolbar_link_earth.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_link_edit.svg create mode 100644 frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart index abfee17d8e..aaecd0ff10 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart @@ -1,10 +1,19 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; 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/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; 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'; @@ -176,4 +185,146 @@ void main() { ); }); }); + + group('document toolbar: link', () { + String? getLinkFromNode(Node node) { + for (final insert in node.delta!) { + final link = insert.attributes?.href; + if (link != null) return link; + } + return null; + } + + bool isPageLink(Node node) { + for (final insert in node.delta!) { + final isPage = insert.attributes?.isPage; + if (isPage == true) return true; + } + return false; + } + + String getNodeText(Node node) { + for (final insert in node.delta!) { + if (insert is TextInsert) return insert.text; + } + return ''; + } + + testWidgets('insert link and remove link', (tester) async { + const text = 'insert link', link = 'https://test.appflowy.cloud'; + await prepareForToolbar(tester, text); + + final toolbar = find.byType(DesktopFloatingToolbar); + expect(toolbar, findsOneWidget); + + /// tap link button to show CreateLinkMenu + final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); + await tester.tapButton(linkButton); + final createLinkMenu = find.byType(LinkCreateMenu); + expect(createLinkMenu, findsOneWidget); + + /// test esc to close + await tester.simulateKeyEvent(LogicalKeyboardKey.escape); + expect(toolbar, findsNothing); + + /// show toolbar again + await selectText(tester, text); + await tester.tapButton(linkButton); + + /// insert link + final textField = find.descendant( + of: createLinkMenu, + matching: find.byType(TextFormField), + ); + await tester.enterText(textField, link); + await tester.pumpAndSettle(); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + Node node = tester.editor.getNodeAtPath([0]); + expect(getLinkFromNode(node), link); + await tester.simulateKeyEvent(LogicalKeyboardKey.escape); + + /// hover link + await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); + final hoverMenu = find.byType(LinkHoverMenu); + expect(hoverMenu, findsOneWidget); + + /// copy link + final copyButton = find.descendant( + of: hoverMenu, + matching: find.byFlowySvg(FlowySvgs.toolbar_link_m), + ); + await tester.tapButton(copyButton); + final clipboardContent = await getIt().getData(); + final plainText = clipboardContent.plainText; + expect(plainText, link); + + /// remove link + await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); + await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m)); + node = tester.editor.getNodeAtPath([0]); + expect(getLinkFromNode(node), null); + }); + + testWidgets('insert link and edit link', (tester) async { + const text = 'edit link', + link = 'https://test.appflowy.cloud', + afterText = '$text after'; + await prepareForToolbar(tester, text); + + /// tap link button to show CreateLinkMenu + final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); + await tester.tapButton(linkButton); + + /// search for page and select it + final textField = find.descendant( + of: find.byType(LinkCreateMenu), + matching: find.byType(TextFormField), + ); + await tester.enterText(textField, gettingStarted); + await tester.pumpAndSettle(); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + await tester.simulateKeyEvent(LogicalKeyboardKey.escape); + + Node node = tester.editor.getNodeAtPath([0]); + expect(isPageLink(node), true); + expect(getLinkFromNode(node) == link, false); + + /// hover link + await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); + + /// click edit button to show LinkEditMenu + final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m); + await tester.tapButton(editButton); + final linkEditMenu = find.byType(LinkEditMenu); + expect(linkEditMenu, findsOneWidget); + + /// change the link text + final titleField = find.descendant( + of: linkEditMenu, + matching: find.byType(TextFormField), + ); + await tester.enterText(titleField, afterText); + await tester.pumpAndSettle(); + await tester.tapButton( + find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)), + ); + final linkField = find.ancestor( + of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()), + matching: find.byType(TextFormField), + ); + await tester.enterText(linkField, link); + await tester.pumpAndSettle(); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + + /// apply the change + final applyButton = + find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr()); + await tester.tapButton(applyButton); + + node = tester.editor.getNodeAtPath([0]); + expect(isPageLink(node), false); + expect(getLinkFromNode(node), link); + expect(getNodeText(node), afterText); + }); + }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 09685aef5c..f445e698aa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -443,8 +443,9 @@ class _AppFlowyEditorPageState extends State color: Theme.of(context).cardColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), - toolbarBuilder: (context, child) => DesktopFloatingToolbar( + toolbarBuilder: (context, child, onDismiss) => DesktopFloatingToolbar( editorState: editorState, + onDismiss: onDismiss, child: child, ), placeHolderBuilder: (_) => customPlaceholderItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart index 4932eb8274..2cc9d700e2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -1,15 +1,20 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'toolbar_cubit.dart'; class DesktopFloatingToolbar extends StatefulWidget { const DesktopFloatingToolbar({ super.key, required this.editorState, required this.child, + required this.onDismiss, }); final EditorState editorState; final Widget child; + final VoidCallback onDismiss; @override State createState() => _DesktopFloatingToolbarState(); @@ -35,11 +40,14 @@ class _DesktopFloatingToolbarState extends State { @override Widget build(BuildContext context) { if (position == null) return Container(); - return Positioned( - left: position!.left, - top: position!.top, - right: position!.right, - child: widget.child, + return BlocProvider( + create: (_) => ToolbarCubit(widget.onDismiss), + child: Positioned( + left: position!.left, + top: position!.top, + right: position!.right, + child: widget.child, + ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart new file mode 100644 index 0000000000..4d3ef71cce --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -0,0 +1,164 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; +import 'package:appflowy/plugins/shared/share/constants.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'link_search_text_field.dart'; + +class LinkCreateMenu extends StatefulWidget { + const LinkCreateMenu({ + super.key, + required this.editorState, + required this.onSubmitted, + required this.onDismiss, + required this.alignment, + }); + + final EditorState editorState; + final void Function(String link, bool isPage) onSubmitted; + final VoidCallback onDismiss; + final LinkMenuAlignment alignment; + + @override + State createState() => _LinkCreateMenuState(); +} + +class _LinkCreateMenuState extends State { + late LinkSearchTextField searchTextField = LinkSearchTextField( + onEnter: () { + searchTextField.onSearchResult( + onLink: () => onSubmittedLink(), + onRecentViews: () => + onSubmittedPageLink(searchTextField.currentRecentView), + onSearchViews: () => + onSubmittedPageLink(searchTextField.currentSearchedView), + onEmpty: () {}, + ); + }, + onEscape: widget.onDismiss, + onDataRefresh: () { + if (mounted) setState(() {}); + }, + ); + + bool get isButtonEnable => searchText.isNotEmpty; + + String get searchText => searchTextField.searchText; + + bool get showAtTop => widget.alignment.isTop; + + @override + void initState() { + super.initState(); + searchTextField.requestFocus(); + searchTextField.searchRecentViews(); + } + + @override + void dispose() { + searchTextField.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 320, + child: Column( + children: showAtTop + ? [ + searchTextField.buildResultContainer( + margin: EdgeInsets.only(bottom: 2), + context: context, + onLinkSelected: onSubmittedLink, + onPageLinkSelected: onSubmittedPageLink, + ), + buildSearchContainer(), + ] + : [ + buildSearchContainer(), + searchTextField.buildResultContainer( + margin: EdgeInsets.only(top: 2), + context: context, + onLinkSelected: onSubmittedLink, + onPageLinkSelected: onSubmittedPageLink, + ), + ], + ), + ); + } + + Widget buildSearchContainer() { + return Container( + width: 320, + height: 48, + decoration: buildToolbarLinkDecoration(context), + padding: EdgeInsets.all(8), + child: ValueListenableBuilder( + valueListenable: searchTextField.textEditingController, + builder: (context, _, __) { + return Row( + children: [ + Expanded(child: searchTextField.buildTextField()), + HSpace(8), + FlowyTextButton( + LocaleKeys.document_toolbar_insert.tr(), + mainAxisAlignment: MainAxisAlignment.center, + padding: EdgeInsets.zero, + constraints: BoxConstraints(maxWidth: 72, minHeight: 32), + fontSize: 14, + fontColor: + isButtonEnable ? Colors.white : LinkStyle.textTertiary, + fillColor: isButtonEnable + ? LinkStyle.fillThemeThick + : LinkStyle.borderColor, + hoverColor: LinkStyle.fillThemeThick, + lineHeight: 20 / 14, + fontWeight: FontWeight.w600, + onPressed: isButtonEnable ? () => onSubmittedLink() : null, + ), + ], + ); + }, + ), + ); + } + + void onSubmittedLink() => widget.onSubmitted(searchText, false); + + void onSubmittedPageLink(ViewPB view) async { + final workspaceId = context + .read() + ?.state + .currentWorkspace + ?.workspaceId ?? + ''; + final link = ShareConstants.buildShareUrl( + workspaceId: workspaceId, + viewId: view.id, + ); + widget.onSubmitted(link, true); + } +} + +ShapeDecoration buildToolbarLinkDecoration(BuildContext context) => + ShapeDecoration( + color: Theme.of(context).cardColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + shadows: [ + const BoxShadow( + color: LinkStyle.shadowMedium, + blurRadius: 24, + offset: Offset(0, 4), + ), + ], + ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart new file mode 100644 index 0000000000..e10b46411b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart @@ -0,0 +1,429 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; +import 'package:appflowy/plugins/shared/share/constants.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'link_create_menu.dart'; +import 'link_search_text_field.dart'; +import 'link_styles.dart'; + +class LinkEditMenu extends StatefulWidget { + const LinkEditMenu({ + super.key, + required this.linkInfo, + required this.onDismiss, + required this.onApply, + required this.onRemoveLink, + }); + + final LinkInfo linkInfo; + final ValueChanged onApply; + final VoidCallback onRemoveLink; + final VoidCallback onDismiss; + + @override + State createState() => _LinkEditMenuState(); +} + +class _LinkEditMenuState extends State { + ValueChanged get onApply => widget.onApply; + + VoidCallback get onRemoveLink => widget.onRemoveLink; + + VoidCallback get onDismiss => widget.onDismiss; + + late TextEditingController linkNameController = + TextEditingController(text: linkInfo.name); + final textFocusNode = FocusNode(); + late LinkInfo linkInfo = widget.linkInfo; + late LinkSearchTextField searchTextField; + bool isShowingSearchResult = false; + ViewPB? currentView; + + bool get enableApply => + linkInfo.link.isNotEmpty && linkNameController.text.isNotEmpty; + + @override + void initState() { + super.initState(); + final isPageLink = linkInfo.isPage; + if (isPageLink) getPageView(); + searchTextField = LinkSearchTextField( + initialSearchText: isPageLink ? '' : linkInfo.link, + onEnter: () { + searchTextField.onSearchResult( + onLink: onLinkSelected, + onRecentViews: () => + onPageSelected(searchTextField.currentRecentView), + onSearchViews: () => + onPageSelected(searchTextField.currentSearchedView), + onEmpty: () { + searchTextField.unfocus(); + }, + ); + }, + onEscape: () { + if (isShowingSearchResult) { + hideSearchResult(); + } else { + onDismiss(); + } + }, + onDataRefresh: () { + if (mounted) setState(() {}); + }, + )..searchRecentViews(); + } + + @override + void dispose() { + linkNameController.dispose(); + textFocusNode.dispose(); + searchTextField.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final showingRecent = + searchTextField.showingRecent && isShowingSearchResult; + return GestureDetector( + onTap: onDismiss, + child: Container( + width: 400, + height: 250 + (showingRecent ? 32 : 0), + color: Colors.white.withAlpha(1), + child: Stack( + children: [ + GestureDetector( + onTap: hideSearchResult, + child: Container( + width: 400, + height: 192, + decoration: buildToolbarLinkDecoration(context), + padding: EdgeInsets.fromLTRB(20, 16, 20, 16), + ), + ), + Positioned( + top: 16, + left: 20, + child: FlowyText.semibold( + LocaleKeys.document_toolbar_pageOrURL.tr(), + color: LinkStyle.textTertiary, + fontSize: 12, + figmaLineHeight: 16, + ), + ), + Positioned( + top: 80, + left: 20, + child: FlowyText.semibold( + LocaleKeys.document_toolbar_linkName.tr(), + color: LinkStyle.textTertiary, + fontSize: 12, + figmaLineHeight: 16, + ), + ), + Positioned( + top: 152, + left: 20, + child: buildButtons(), + ), + Positioned( + top: 108, + left: 20, + child: buildNameTextField(), + ), + Positioned( + top: 36, + left: 20, + child: buildLinkField(), + ), + ], + ), + ), + ); + } + + Widget buildLinkField() { + final showPageView = linkInfo.isPage && !isShowingSearchResult; + if (showPageView) return buildPageView(); + if (!isShowingSearchResult) return buildLinkView(); + return SizedBox( + width: 360, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 360, + height: 32, + child: searchTextField.buildTextField( + autofocus: true, + ), + ), + VSpace(6), + searchTextField.buildResultContainer( + context: context, + width: 360, + onPageLinkSelected: onPageSelected, + onLinkSelected: onLinkSelected, + ), + ], + ), + ); + } + + Widget buildButtons() { + return GestureDetector( + onTap: hideSearchResult, + child: SizedBox( + width: 360, + height: 32, + child: Row( + children: [ + FlowyIconButton( + icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), + width: 32, + height: 32, + tooltipText: LocaleKeys.editor_removeLink.tr(), + preferBelow: false, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + border: Border.all(color: LinkStyle.borderColor), + ), + onPressed: onRemoveLink, + ), + Spacer(), + DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + border: Border.all(color: LinkStyle.borderColor), + ), + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + padding: EdgeInsets.zero, + mainAxisAlignment: MainAxisAlignment.center, + constraints: BoxConstraints(maxWidth: 78, minHeight: 32), + fontSize: 14, + lineHeight: 20 / 14, + fontColor: LinkStyle.textPrimary, + fillColor: Colors.transparent, + fontWeight: FontWeight.w400, + onPressed: onDismiss, + ), + ), + HSpace(12), + ValueListenableBuilder( + valueListenable: linkNameController, + builder: (context, _, __) { + return FlowyTextButton( + LocaleKeys.settings_appearance_documentSettings_apply.tr(), + padding: EdgeInsets.zero, + mainAxisAlignment: MainAxisAlignment.center, + constraints: BoxConstraints(maxWidth: 78, minHeight: 32), + fontSize: 14, + lineHeight: 20 / 14, + hoverColor: LinkStyle.fillThemeThick.withAlpha(200), + fontColor: + enableApply ? Colors.white : LinkStyle.textTertiary, + fillColor: enableApply + ? LinkStyle.fillThemeThick + : LinkStyle.borderColor, + fontWeight: FontWeight.w400, + onPressed: + enableApply ? () => widget.onApply.call(linkInfo) : null, + ); + }, + ), + ], + ), + ), + ); + } + + Widget buildNameTextField() { + return SizedBox( + width: 360, + height: 32, + child: TextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + focusNode: textFocusNode, + textAlign: TextAlign.left, + controller: linkNameController, + style: TextStyle( + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w400, + ), + onChanged: (text) { + linkInfo = LinkInfo( + name: text, + link: linkInfo.link, + isPage: linkInfo.isPage, + ); + }, + decoration: LinkStyle.buildLinkTextFieldInputDecoration( + LocaleKeys.document_toolbar_linkNameHint.tr(), + ), + ), + ); + } + + Widget buildPageView() { + late Widget child; + final view = currentView; + if (view == null) { + child = Center( + child: SizedBox.fromSize( + size: Size(10, 10), + child: CircularProgressIndicator(), + ), + ); + } else { + child = GestureDetector( + onTap: showSearchResult, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + height: 32, + padding: EdgeInsets.fromLTRB(8, 6, 8, 6), + child: Row( + children: [ + searchTextField.buildIcon(view), + HSpace(8), + Flexible( + child: FlowyText.regular( + view.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } + return Container( + width: 360, + height: 32, + decoration: buildDecoration(), + child: child, + ); + } + + Widget buildLinkView() { + return Container( + width: 360, + height: 32, + decoration: buildDecoration(), + child: GestureDetector( + onTap: showSearchResult, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: EdgeInsets.fromLTRB(8, 6, 8, 6), + child: Row( + children: [ + FlowySvg(FlowySvgs.toolbar_link_earth_m), + HSpace(8), + Flexible( + child: FlowyText.regular( + linkInfo.link, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 20, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Future getPageView() async { + if (!linkInfo.isPage) return; + final link = linkInfo.link; + final viewId = link.split('/').lastOrNull ?? ''; + final (view, isInTrash, isDeleted) = + await ViewBackendService.getMentionPageStatus(viewId); + if (mounted) { + setState(() { + currentView = view; + }); + } + } + + void showSearchResult() { + setState(() { + if (linkInfo.isPage) searchTextField.updateText(''); + isShowingSearchResult = true; + searchTextField.requestFocus(); + }); + } + + void hideSearchResult() { + setState(() { + isShowingSearchResult = false; + searchTextField.unfocus(); + textFocusNode.unfocus(); + }); + } + + void onLinkSelected() { + if (mounted) { + linkInfo = LinkInfo( + name: linkInfo.name, + link: searchTextField.searchText, + ); + hideSearchResult(); + } + } + + Future onPageSelected(ViewPB view) async { + currentView = view; + final link = ShareConstants.buildShareUrl( + workspaceId: await UserBackendService.getCurrentWorkspace().fold( + (s) => s.id, + (f) => '', + ), + viewId: view.id, + ); + linkInfo = LinkInfo( + name: linkInfo.name, + link: link, + isPage: true, + ); + searchTextField.updateText(linkInfo.link); + if (mounted) { + setState(() { + isShowingSearchResult = false; + searchTextField.unfocus(); + }); + } + } + + BoxDecoration buildDecoration() => BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: LinkStyle.borderColor), + ); +} + +class LinkInfo { + LinkInfo({this.isPage = false, required this.name, required this.link}); + + final bool isPage; + final String name; + final String link; + + Attributes toAttribute() => + {AppFlowyRichTextKeys.href: link, kIsPageLink: isPage}; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart new file mode 100644 index 0000000000..e5950b5e79 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -0,0 +1,322 @@ +import 'dart:math'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +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/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/view/view_service.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'; +import 'package:flutter/services.dart'; + +import 'link_create_menu.dart'; +import 'link_edit_menu.dart'; +import 'link_styles.dart'; + +class LinkHoverTrigger extends StatefulWidget { + const LinkHoverTrigger({ + super.key, + required this.editorState, + required this.selection, + required this.node, + required this.attribute, + required this.size, + this.delayToShow = const Duration(milliseconds: 50), + this.delayToHide = const Duration(milliseconds: 300), + }); + + final EditorState editorState; + final Selection selection; + final Node node; + final Attributes attribute; + final Size size; + final Duration delayToShow; + final Duration delayToHide; + + @override + State createState() => _LinkHoverTriggerState(); +} + +class _LinkHoverTriggerState extends State { + final hoverMenuController = PopoverController(); + final editMenuController = PopoverController(); + bool isHoverMenuShowing = false; + bool isHoverMenuHovering = false; + bool isHoverTriggerHovering = false; + + Size get size => widget.size; + + EditorState get editorState => widget.editorState; + + Selection get selection => widget.selection; + + Attributes get attribute => widget.attribute; + + @override + void dispose() { + hoverMenuController.close(); + editMenuController.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (v) { + isHoverTriggerHovering = true; + Future.delayed(widget.delayToShow, () { + if (isHoverTriggerHovering && !isHoverMenuShowing) { + showLinkHoverMenu(); + } + }); + }, + onExit: (v) { + isHoverTriggerHovering = false; + tryToDismissLinkHoverMenu(); + }, + child: buildHoverPopover( + buildEditPopover( + Container( + color: Colors.black.withAlpha(1), + width: size.width, + height: size.height, + ), + ), + ), + ); + } + + Widget buildHoverPopover(Widget child) { + return AppFlowyPopover( + controller: hoverMenuController, + direction: PopoverDirection.topWithLeftAligned, + offset: Offset(0, size.height), + onOpen: () { + keepEditorFocusNotifier.increase(); + isHoverMenuShowing = true; + }, + onClose: () { + keepEditorFocusNotifier.decrease(); + isHoverMenuShowing = false; + }, + margin: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: max(320, size.width), + maxHeight: 48 + size.height, + ), + decorationColor: Colors.transparent, + popoverDecoration: BoxDecoration(), + popupBuilder: (context) => LinkHoverMenu( + attribute: widget.attribute, + triggerSize: size, + onEnter: (_) { + isHoverMenuHovering = true; + }, + onExit: (_) { + isHoverMenuHovering = false; + tryToDismissLinkHoverMenu(); + }, + onOpenLink: openLink, + onCopyLink: copyLink, + onEditLink: showLinkEditMenu, + onRemoveLink: () => removeLink(editorState, selection, true), + ), + child: child, + ); + } + + Widget buildEditPopover(Widget child) { + final href = attribute.href ?? '', + isPage = attribute.isPage, + title = editorState.getTextInSelection(selection).join(); + return AppFlowyPopover( + controller: editMenuController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: Offset(0, 0), + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + margin: EdgeInsets.zero, + asBarrier: true, + decorationColor: Colors.transparent, + popoverDecoration: BoxDecoration(), + constraints: BoxConstraints( + maxWidth: 400, + minHeight: 282, + ), + popupBuilder: (context) => LinkEditMenu( + linkInfo: LinkInfo(name: title, link: href, isPage: isPage), + onDismiss: () => editMenuController.close(), + onApply: (info) async { + final transaction = editorState.transaction; + transaction.replaceText( + widget.node, + selection.startIndex, + selection.length, + info.name, + attributes: info.toAttribute(), + ); + editMenuController.close(); + await editorState.apply(transaction); + }, + onRemoveLink: () => removeLink(editorState, selection, true), + ), + child: child, + ); + } + + void showLinkHoverMenu() { + if (isHoverMenuShowing) return; + keepEditorFocusNotifier.increase(); + hoverMenuController.show(); + } + + void showLinkEditMenu() { + keepEditorFocusNotifier.increase(); + hoverMenuController.close(); + editMenuController.show(); + } + + void tryToDismissLinkHoverMenu() { + Future.delayed(widget.delayToHide, () { + if (isHoverMenuHovering || isHoverTriggerHovering) { + return; + } + hoverMenuController.close(); + }); + } + + Future openLink() async { + final href = widget.attribute.href ?? '', isPage = widget.attribute.isPage; + + if (isPage) { + final viewId = href.split('/').lastOrNull ?? ''; + if (viewId.isEmpty) { + await afLaunchUrlString(href); + } else { + final (view, isInTrash, isDeleted) = + await ViewBackendService.getMentionPageStatus(viewId); + if (view != null) { + await handleMentionBlockTap(context, widget.editorState, view); + } + } + } else { + await afLaunchUrlString(href); + } + } + + Future copyLink() async { + final href = widget.attribute.href ?? ''; + if (href.isEmpty) return; + await getIt() + .setData(ClipboardServiceData(plainText: href)); + hoverMenuController.close(); + } +} + +class LinkHoverMenu extends StatelessWidget { + const LinkHoverMenu({ + super.key, + required this.attribute, + required this.onEnter, + required this.onExit, + required this.triggerSize, + required this.onCopyLink, + required this.onOpenLink, + required this.onEditLink, + required this.onRemoveLink, + }); + + final Attributes attribute; + final PointerEnterEventListener onEnter; + final PointerExitEventListener onExit; + final Size triggerSize; + final VoidCallback onCopyLink; + final VoidCallback onOpenLink; + final VoidCallback onEditLink; + final VoidCallback onRemoveLink; + + @override + Widget build(BuildContext context) { + final href = attribute.href ?? ''; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MouseRegion( + onEnter: onEnter, + onExit: onExit, + child: SizedBox( + width: max(320, triggerSize.width), + height: 48, + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: 320, + height: 48, + decoration: buildToolbarLinkDecoration(context), + padding: EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: FlowyText.regular( + href, + fontSize: 14, + figmaLineHeight: 20, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + height: 20, + width: 1, + color: LinkStyle.borderColor, + margin: EdgeInsets.symmetric(horizontal: 6), + ), + FlowyIconButton( + icon: FlowySvg(FlowySvgs.toolbar_link_m), + tooltipText: LocaleKeys.editor_copyLink.tr(), + width: 36, + height: 32, + onPressed: onCopyLink, + ), + FlowyIconButton( + icon: FlowySvg(FlowySvgs.toolbar_link_edit_m), + tooltipText: LocaleKeys.editor_editLink.tr(), + width: 36, + height: 32, + onPressed: onEditLink, + ), + FlowyIconButton( + icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), + tooltipText: LocaleKeys.editor_removeLink.tr(), + width: 36, + height: 32, + onPressed: onRemoveLink, + ), + ], + ), + ), + ), + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: onEnter, + onExit: onExit, + child: GestureDetector( + onTap: onOpenLink, + child: Container( + width: triggerSize.width, + height: triggerSize.height, + color: Colors.black.withAlpha(1), + ), + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart new file mode 100644 index 0000000000..4fcd9e3b8b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart @@ -0,0 +1,326 @@ +import 'dart:math'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/list_extension.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'link_create_menu.dart'; +import 'link_styles.dart'; + +class LinkSearchTextField { + LinkSearchTextField({ + this.onEscape, + this.onEnter, + this.onDataRefresh, + String? initialSearchText, + }) : textEditingController = TextEditingController( + text: initialSearchText ?? '', + ); + + final TextEditingController textEditingController; + final ItemScrollController searchController = ItemScrollController(); + late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); + final List searchedViews = []; + final List recentViews = []; + int selectedIndex = 0; + + final VoidCallback? onEscape; + final VoidCallback? onEnter; + final VoidCallback? onDataRefresh; + + String get searchText => textEditingController.text; + + bool get isButtonEnable => searchText.isNotEmpty; + + bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty; + + ViewPB get currentSearchedView => searchedViews[selectedIndex]; + + ViewPB get currentRecentView => recentViews[selectedIndex]; + + void dispose() { + textEditingController.dispose(); + focusNode.dispose(); + searchedViews.clear(); + recentViews.clear(); + } + + Widget buildTextField({bool autofocus = false}) { + return TextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + autofocus: autofocus, + focusNode: focusNode, + textAlign: TextAlign.left, + controller: textEditingController, + style: TextStyle( + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w400, + ), + onChanged: (text) { + if (text.isEmpty) { + searchedViews.clear(); + selectedIndex = 0; + onDataRefresh?.call(); + } else { + searchViews(text); + } + }, + decoration: LinkStyle.buildLinkTextFieldInputDecoration( + LocaleKeys.document_toolbar_linkInputHint.tr(), + ), + ); + } + + Widget buildResultContainer({ + EdgeInsetsGeometry? margin, + required BuildContext context, + VoidCallback? onLinkSelected, + ValueChanged? onPageLinkSelected, + double width = 320.0, + }) { + return onSearchResult( + onEmpty: () => SizedBox.shrink(), + onLink: () => Container( + height: 48, + width: width, + padding: EdgeInsets.all(8), + margin: margin, + decoration: buildToolbarLinkDecoration(context), + child: FlowyButton( + leftIcon: FlowySvg(FlowySvgs.toolbar_link_earth_m), + isSelected: true, + text: FlowyText.regular( + searchText, + overflow: TextOverflow.ellipsis, + fontSize: 14, + figmaLineHeight: 20, + ), + onTap: onLinkSelected, + ), + ), + onRecentViews: () => Container( + width: width, + height: recentViews.length.clamp(1, 5) * 32.0 + 48, + margin: margin, + padding: EdgeInsets.all(8), + decoration: buildToolbarLinkDecoration(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 32, + padding: EdgeInsets.all(8), + child: FlowyText.semibold( + LocaleKeys.inlineActions_recentPages.tr(), + color: LinkStyle.textTertiary, + fontSize: 12, + figmaLineHeight: 16, + ), + ), + Flexible( + child: ListView.builder( + itemBuilder: (context, index) { + final currentView = recentViews[index]; + return buildPageItem( + currentView, + index == selectedIndex, + onPageLinkSelected, + ); + }, + itemCount: recentViews.length, + ), + ), + ], + ), + ), + onSearchViews: () => Container( + width: width, + height: searchedViews.length.clamp(1, 5) * 32.0 + 16, + margin: margin, + decoration: buildToolbarLinkDecoration(context), + child: ScrollablePositionedList.builder( + padding: EdgeInsets.all(8), + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: searchedViews.length, + itemScrollController: searchController, + initialScrollIndex: max(0, selectedIndex), + itemBuilder: (context, index) { + final currentView = searchedViews[index]; + return buildPageItem( + currentView, + index == selectedIndex, + onPageLinkSelected, + ); + }, + ), + ), + ); + } + + Widget buildPageItem( + ViewPB view, + bool isSelected, + ValueChanged? onSubmittedPageLink, + ) { + return SizedBox( + height: 32, + child: FlowyButton( + isSelected: isSelected, + leftIcon: buildIcon(view), + text: FlowyText.regular( + view.name, + overflow: TextOverflow.ellipsis, + fontSize: 14, + figmaLineHeight: 20, + ), + onTap: () => onSubmittedPageLink?.call(view), + ), + ); + } + + Widget buildIcon(ViewPB view) { + if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20)); + final iconData = view.icon.toEmojiIconData(); + return RawEmojiIconWidget( + emoji: iconData, + emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20, + lineHeight: 1, + ); + } + + void requestFocus() => focusNode.requestFocus(); + + void unfocus() => focusNode.unfocus(); + + void updateText(String text) => textEditingController.text = text; + + T onSearchResult({ + required ValueGetter onLink, + required ValueGetter onRecentViews, + required ValueGetter onSearchViews, + required ValueGetter onEmpty, + }) { + if (searchedViews.isEmpty && recentViews.isEmpty && searchText.isEmpty) { + return onEmpty.call(); + } + if (searchedViews.isEmpty && searchText.isNotEmpty) { + return onLink.call(); + } + if (searchedViews.isEmpty) return onRecentViews.call(); + return onSearchViews.call(); + } + + KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { + if (key is! KeyDownEvent) return KeyEventResult.ignored; + int index = selectedIndex; + if (key.logicalKey == LogicalKeyboardKey.escape) { + onEscape?.call(); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.arrowUp) { + index = onSearchResult( + onLink: () => 0, + onRecentViews: () { + int result = index - 1; + if (result < 0) result = recentViews.length - 1; + return result; + }, + onSearchViews: () { + int result = index - 1; + if (result < 0) result = searchedViews.length - 1; + searchController.scrollTo( + index: result, + alignment: 0.5, + duration: const Duration(milliseconds: 300), + ); + return result; + }, + onEmpty: () => 0, + ); + refreshIndex(index); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.arrowDown) { + index = onSearchResult( + onLink: () => 0, + onRecentViews: () { + int result = index + 1; + if (result >= recentViews.length) result = 0; + return result; + }, + onSearchViews: () { + int result = index + 1; + if (result >= searchedViews.length) result = 0; + searchController.scrollTo( + index: result, + alignment: 0.5, + duration: const Duration(milliseconds: 300), + ); + return result; + }, + onEmpty: () => 0, + ); + refreshIndex(index); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.enter) { + onEnter?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + + Future searchRecentViews() async { + final recentService = getIt(); + final sectionViews = await recentService.recentViews(); + final views = sectionViews + .unique((e) => e.item.id) + .map((e) => e.item) + .take(5) + .toList(); + recentViews.clear(); + recentViews.addAll(views); + selectedIndex = 0; + onDataRefresh?.call(); + } + + Future searchViews(String search) async { + final viewResult = await ViewBackendService.getAllViews(); + final allViews = viewResult + .toNullable() + ?.items + .where( + (view) => + view.name.toLowerCase().contains(search.toLowerCase()) || + (view.name.isEmpty && search.isEmpty) || + (view.name.isEmpty && + LocaleKeys.menuAppHeader_defaultNewPageName + .tr() + .toLowerCase() + .contains(search.toLowerCase())), + ) + .take(10) + .toList(); + searchedViews.clear(); + searchedViews.addAll(allViews ?? []); + selectedIndex = 0; + onDataRefresh?.call(); + } + + void refreshIndex(int index) { + selectedIndex = index; + onDataRefresh?.call(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart new file mode 100644 index 0000000000..ee63269dfc --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class LinkStyle { + static const borderColor = Color(0xFFE8ECF3); + static const textTertiary = Color(0xFF99A1A8); + static const fillThemeThick = Color(0xFF00B5FF); + static const shadowMedium = Color(0x1F22251F); + static const textPrimary = Color(0xFF1F2329); + + static InputDecoration buildLinkTextFieldInputDecoration(String hintText) { + const border = OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide(color: LinkStyle.borderColor), + ); + final enableBorder = border.copyWith( + borderSide: BorderSide(color: LinkStyle.fillThemeThick), + ); + const hintStyle = TextStyle( + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w400, + color: LinkStyle.textTertiary, + ); + return InputDecoration( + hintText: hintText, + hintStyle: hintStyle, + contentPadding: const EdgeInsets.fromLTRB(8, 6, 8, 6), + isDense: true, + border: border, + enabledBorder: border, + focusedBorder: enableBorder, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart new file mode 100644 index 0000000000..8e114bf4c8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +import 'package:bloc/bloc.dart'; + +class ToolbarCubit extends Cubit { + ToolbarCubit(this.onDismissCallback) : super(ToolbarState._()); + + final VoidCallback onDismissCallback; + + void dismiss() { + onDismissCallback.call(); + } +} + +class ToolbarState { + const ToolbarState._(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 33333a3e92..91ae1af354 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -71,11 +71,13 @@ class RawEmojiIconWidget extends StatefulWidget { required this.emoji, required this.emojiSize, this.enableColor = true, + this.lineHeight, }); final EmojiIconData emoji; final double emojiSize; final bool enableColor; + final double? lineHeight; @override State createState() => _RawEmojiIconWidgetState(); @@ -115,6 +117,7 @@ class _RawEmojiIconWidgetState extends State { emoji: widget.emoji.emoji, fontSize: widget.emojiSize, textAlign: TextAlign.justify, + lineHeight: widget.lineHeight, ); case FlowyIconType.icon: IconsData iconData = diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index 398d158e9a..23c05b89eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -118,7 +118,7 @@ class _MentionPageBlockState extends State { view: view, content: state.blockContent, textStyle: widget.textStyle, - handleTap: () => _handleTap( + handleTap: () => handleMentionBlockTap( context, widget.editorState, view, @@ -138,7 +138,7 @@ class _MentionPageBlockState extends State { content: state.blockContent, textStyle: widget.textStyle, showTrashHint: state.isInTrash, - handleTap: () => _handleTap( + handleTap: () => handleMentionBlockTap( context, widget.editorState, view, @@ -221,7 +221,8 @@ class _MentionSubPageBlockState extends State { view: view, showTrashHint: state.isInTrash, textStyle: widget.textStyle, - handleTap: () => _handleTap(context, widget.editorState, view), + handleTap: () => + handleMentionBlockTap(context, widget.editorState, view), isChildPage: true, content: '', handleDoubleTap: () => _handleDoubleTap( @@ -239,7 +240,8 @@ class _MentionSubPageBlockState extends State { content: null, textStyle: widget.textStyle, isChildPage: true, - handleTap: () => _handleTap(context, widget.editorState, view), + handleTap: () => + handleMentionBlockTap(context, widget.editorState, view), ); } }, @@ -321,7 +323,7 @@ Path? _findNodePathByBlockId(EditorState editorState, String blockId) { return null; } -Future _handleTap( +Future handleMentionBlockTap( BuildContext context, EditorState editorState, ViewPB view, { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index c7fa1a34cc..355e7b3b7a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -1,11 +1,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'toolbar_id_enum.dart'; +const kIsPageLink = 'is_page_link'; + final customLinkItem = ToolbarItem( id: ToolbarId.link.id, group: 4, @@ -22,6 +27,7 @@ final customLinkItem = ToolbarItem( final hoverColor = isHref ? highlightColor : EditorStyleCustomizer.toolbarHoverColor(context); + final toolbarCubit = context.read(); final child = FlowyIconButton( width: 36, @@ -33,7 +39,14 @@ final customLinkItem = ToolbarItem( size: Size.square(20.0), color: Theme.of(context).iconTheme.color, ), - onPressed: () => showLinkMenu(context, editorState, selection, isHref), + onPressed: () { + toolbarCubit?.dismiss(); + if (isHref) { + removeLink(editorState, selection, isHref); + } else { + _showLinkMenu(context, editorState, selection, isHref); + } + }, ); if (tooltipBuilder != null) { @@ -48,3 +61,159 @@ final customLinkItem = ToolbarItem( return child; }, ); + +void removeLink( + EditorState editorState, + Selection selection, + bool isHref, +) { + if (!isHref) return; + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final index = selection.normalized.startIndex; + final length = selection.length; + final transaction = editorState.transaction + ..formatText( + node, + index, + length, + { + BuiltInAttributeKey.href: null, + kIsPageLink: null, + }, + ); + editorState.apply(transaction); +} + +void _showLinkMenu( + BuildContext context, + EditorState editorState, + Selection selection, + bool isHref, +) { + final (left, top, right, bottom, alignment) = _getPosition(editorState); + + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + + OverlayEntry? overlay; + + void dismissOverlay() { + keepEditorFocusNotifier.decrease(); + overlay?.remove(); + overlay = null; + } + + keepEditorFocusNotifier.increase(); + overlay = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: left, + right: right, + dismissCallback: () => keepEditorFocusNotifier.decrease(), + builder: (context) { + return LinkCreateMenu( + alignment: alignment, + editorState: editorState, + onSubmitted: (link, isPage) async { + await editorState.formatDelta(selection, { + BuiltInAttributeKey.href: link, + kIsPageLink: isPage, + }); + dismissOverlay(); + }, + onDismiss: dismissOverlay, + ); + }, + ).build(); + + Overlay.of(context, rootOverlay: true).insert(overlay!); +} + +extension AttributeExtension on Attributes { + bool get isPage { + if (this[kIsPageLink] is bool) { + return this[kIsPageLink]; + } + return false; + } +} + +// get a proper position for link menu +( + double? left, + double? top, + double? right, + double? bottom, + LinkMenuAlignment alignment, +) _getPosition( + EditorState editorState, +) { + final rect = editorState.selectionRects().first; + const menuHeight = 222.0, menuWidth = 320.0; + + double? left, right, top, bottom; + LinkMenuAlignment alignment = LinkMenuAlignment.topLeft; + final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero), + editorSize = editorState.renderBox!.size; + final editorBottom = editorSize.height + editorOffset.dy, + editorRight = editorSize.width + editorOffset.dx; + final overflowBottom = rect.bottom + menuHeight > editorBottom, + overflowTop = rect.top - menuHeight < 0, + overflowLeft = rect.left - menuWidth < 0, + overflowRight = rect.right + menuWidth > editorRight; + + if (overflowTop && !overflowBottom) { + /// show at bottom + top = rect.bottom; + } else if (overflowBottom && !overflowTop) { + /// show at top + bottom = editorBottom - rect.top; + } else if (!overflowTop && !overflowBottom) { + /// show at bottom + top = rect.bottom; + } else { + top = 0; + } + + if (overflowLeft && !overflowRight) { + /// show at right + left = rect.left; + } else if (overflowRight && !overflowLeft) { + /// show at left + right = editorRight - rect.right; + } else if (!overflowLeft && !overflowRight) { + /// show at right + left = rect.left; + } else { + left = 0; + } + + if (left != null && top != null) { + alignment = LinkMenuAlignment.bottomRight; + } else if (left != null && bottom != null) { + alignment = LinkMenuAlignment.topRight; + } else if (right != null && top != null) { + alignment = LinkMenuAlignment.bottomLeft; + } else if (right != null && bottom != null) { + alignment = LinkMenuAlignment.topLeft; + } + + return (left, top, right, bottom, alignment); +} + +enum LinkMenuAlignment { + topLeft, + topRight, + bottomLeft, + bottomRight, +} + +extension LinkMenuAlignmentExtension on LinkMenuAlignment { + bool get isTop => + this == LinkMenuAlignment.topLeft || this == LinkMenuAlignment.topRight; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 4b4b2d135a..c602ade2c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -28,6 +28,7 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; class EditorStyleCustomizer { @@ -134,6 +135,7 @@ class EditorStyleCustomizer { textSpanDecorator: customizeAttributeDecorator, textScaleFactor: context.watch().state.textScaleFactor, + textSpanOverlayBuilder: _buildTextSpanOverlay, ); } @@ -468,14 +470,15 @@ class EditorStyleCustomizer { ); } - return defaultTextSpanDecoratorForAttribute( - context, - node, - index, - text, - before, - after, - ); + if (href != null) { + return TextSpan( + style: before.style, + text: text.text, + mouseCursor: SystemMouseCursors.click, + ); + } else { + return before; + } } Widget buildToolbarItemTooltip( @@ -590,4 +593,54 @@ class EditorStyleCustomizer { _ => style, }; } + + List _buildTextSpanOverlay( + BuildContext context, + Node node, + SelectableMixin delegate, + ) { + final delta = node.delta; + if (delta == null) { + return []; + } + final widgets = []; + final textInserts = delta.whereType(); + int index = 0; + final editorState = context.read(); + for (final textInsert in textInserts) { + if (textInsert.attributes?.href != null) { + final nodeSelection = Selection( + start: Position(path: node.path, offset: index), + end: Position( + path: node.path, + offset: index + textInsert.length, + ), + ); + final rectList = delegate.getRectsInSelection(nodeSelection); + if (rectList.isNotEmpty) { + for (final rect in rectList) { + widgets.add( + Positioned( + left: rect.left, + top: rect.top, + child: SizedBox( + width: rect.width, + height: rect.height, + child: LinkHoverTrigger( + editorState: editorState, + selection: nodeSelection, + attribute: textInsert.attributes!, + node: node, + size: rect.size, + ), + ), + ), + ); + } + } + } + index += textInsert.length; + } + return widgets; + } } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 9d25672622..0ef500d967 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -25,6 +25,7 @@ class AppFlowyPopover extends StatelessWidget { this.skipTraversal = false, this.decorationColor, this.borderRadius, + this.popoverDecoration, this.animationDuration = const Duration(), this.slideDistance = 5.0, this.beginScaleFactor = 0.9, @@ -56,6 +57,7 @@ class AppFlowyPopover extends StatelessWidget { final double endScaleFactor; final double beginOpacity; final double endOpacity; + final Decoration? popoverDecoration; /// The widget that will be used to trigger the popover. /// @@ -102,6 +104,7 @@ class AppFlowyPopover extends StatelessWidget { popupBuilder: (context) => _PopoverContainer( constraints: constraints, margin: margin, + decoration: popoverDecoration, decorationColor: decorationColor, borderRadius: borderRadius, child: popupBuilder(context), @@ -116,6 +119,7 @@ class _PopoverContainer extends StatelessWidget { const _PopoverContainer({ this.decorationColor, this.borderRadius, + this.decoration, required this.child, required this.margin, required this.constraints, @@ -126,6 +130,7 @@ class _PopoverContainer extends StatelessWidget { final EdgeInsets margin; final Color? decorationColor; final BorderRadius? borderRadius; + final Decoration? decoration; @override Widget build(BuildContext context) { @@ -133,10 +138,11 @@ class _PopoverContainer extends StatelessWidget { type: MaterialType.transparency, child: Container( padding: margin, - decoration: context.getPopoverDecoration( - color: decorationColor, - borderRadius: borderRadius, - ), + decoration: decoration ?? + context.getPopoverDecoration( + color: decorationColor, + borderRadius: borderRadius, + ), constraints: constraints, child: child, ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 4d580a4fd9..dcaefebed4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5070212" - resolved-ref: "5070212ee0f02182a8acdd760b4d7b42264baec4" + ref: "50f9724" + resolved-ref: "50f9724190ee47eac3d7dbe323a3ce39d19ea883" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 25ce68a289..19e30fa4a2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "5070212" + ref: "50f9724" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg b/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg new file mode 100644 index 0000000000..57cb67da9a --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg b/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg new file mode 100644 index 0000000000..fc8765fa5b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg b/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg new file mode 100644 index 0000000000..e1061b914a --- /dev/null +++ b/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7d8cba8b13..2c4dfa4371 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2120,7 +2120,12 @@ "inlineCode": "Inline code", "suggestions": "Suggestions", "turnInto": "Turn into", - "equation": "Equation" + "equation": "Equation", + "insert": "Insert", + "linkInputHint": "Paste link or search pages", + "pageOrURL": "Page or URL", + "linkName": "Link Name", + "linkNameHint": "Input link name" }, "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", From 910c45e45741844d4027bbf7b85882d3a97851d5 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 24 Mar 2025 09:55:58 +0800 Subject: [PATCH 194/384] chore: change some mobile slash menu icons (#7579) --- .../slash_menu_items/image_item.dart | 25 +++++----- .../slash_menu_items/mobile_items.dart | 6 +-- .../slash_menu_items/sub_page_item.dart | 49 ++++++++++--------- .../flowy_icons/20x/slash_menu_image.svg | 5 ++ 4 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 frontend/resources/flowy_icons/20x/slash_menu_image.svg diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart index f0ce852e41..844b73c3e1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart @@ -17,17 +17,20 @@ final _keywords = [ ]; /// Image menu item -final imageSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_image.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertImageBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_image_s, - isSelected: isSelected, - style: style, - ), -); +final imageSlashMenuItem = buildImageSlashMenuItem(); + +SelectionMenuItem buildImageSlashMenuItem({FlowySvgData? svg}) => + SelectionMenuItem( + getName: () => LocaleKeys.document_slashMenu_name_image.tr(), + keywords: _keywords, + handler: (editorState, _, __) async => editorState.insertImageBlock(), + nameBuilder: slashMenuItemNameBuilder, + icon: (editorState, isSelected, style) => SelectableSvgWidget( + data: svg ?? FlowySvgs.slash_menu_icon_image_s, + isSelected: isSelected, + style: style, + ), + ); extension on EditorState { Future insertImageBlock() async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart index b69e74bd74..b71b54ad40 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart @@ -15,7 +15,7 @@ final List mobileItems = [ mobileTableSlashMenuItem, visualsMobileSlashMenuItem, dateOrReminderSlashMenuItem, - subPageSlashMenuItem, + buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), advancedMobileSlashMenuItem, ]; @@ -26,7 +26,7 @@ final List mobileItemsInTale = [ fileAndMediaMobileSlashMenuItem, visualsMobileSlashMenuItem, dateOrReminderSlashMenuItem, - subPageSlashMenuItem, + buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), advancedMobileSlashMenuItem, ]; @@ -93,7 +93,7 @@ MobileSelectionMenuItem fileAndMediaMobileSlashMenuItem = ), nameBuilder: slashMenuItemNameBuilder, children: [ - imageSlashMenuItem, + buildImageSlashMenuItem(svg: FlowySvgs.slash_menu_image_m), photoGallerySlashMenuItem, fileSlashMenuItem, ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart index bc7f7e46b4..1052dbbe3e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart @@ -22,26 +22,29 @@ final _keywords = [ ]; // Sub-page menu item -SelectionMenuItem subPageSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(), - keywords: _keywords, - updateSelection: (editorState, path, __, ___) { - final context = editorState.document.root.context; - if (context != null) { - final isInDatabase = - context.read().isInDatabaseRowPage; - if (isInDatabase) { - Navigator.of(context).pop(); - } - } - return Selection.collapsed(Position(path: path)); - }, - replace: (_, node) => node.delta?.isEmpty ?? false, - nodeBuilder: (_, __) => subPageNode(), - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.insert_document_s, - isSelected: isSelected, - style: style, - ), -); +SelectionMenuItem subPageSlashMenuItem = buildSubpageSlashMenuItem(); + +SelectionMenuItem buildSubpageSlashMenuItem({FlowySvgData? svg}) => + SelectionMenuItem.node( + getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(), + keywords: _keywords, + updateSelection: (editorState, path, __, ___) { + final context = editorState.document.root.context; + if (context != null) { + final isInDatabase = + context.read().isInDatabaseRowPage; + if (isInDatabase) { + Navigator.of(context).pop(); + } + } + return Selection.collapsed(Position(path: path)); + }, + replace: (_, node) => node.delta?.isEmpty ?? false, + nodeBuilder: (_, __) => subPageNode(), + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: svg ?? FlowySvgs.insert_document_s, + isSelected: isSelected, + style: style, + ), + ); diff --git a/frontend/resources/flowy_icons/20x/slash_menu_image.svg b/frontend/resources/flowy_icons/20x/slash_menu_image.svg new file mode 100644 index 0000000000..f5b7917ad3 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/slash_menu_image.svg @@ -0,0 +1,5 @@ + + + + + From 08d1d3602ebb9e13a46af07e6e334156333ea1d1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 10:44:20 +0800 Subject: [PATCH 195/384] chore: implement MCP client --- frontend/rust-lib/Cargo.lock | 158 ++++++++++++++++++ frontend/rust-lib/flowy-ai/Cargo.toml | 1 + frontend/rust-lib/flowy-ai/src/lib.rs | 1 + .../flowy-ai/src/mcp/client_manager.rs | 11 ++ frontend/rust-lib/flowy-ai/src/mcp/mod.rs | 1 + 5 files changed, 172 insertions(+) create mode 100644 frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs create mode 100644 frontend/rust-lib/flowy-ai/src/mcp/mod.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 60d65be5cc..4d19fb2c2f 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -154,6 +154,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.94" @@ -257,6 +306,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -764,6 +819,12 @@ dependencies = [ "phf_codegen 0.11.2", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "cipher" version = "0.4.4" @@ -785,6 +846,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "client-api" version = "0.2.0" @@ -1181,6 +1282,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2041,6 +2148,7 @@ dependencies = [ "lib-dispatch", "lib-infra", "log", + "mcpr", "md5", "notify", "pin-project", @@ -3088,6 +3196,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -3681,6 +3795,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -4088,6 +4208,25 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +[[package]] +name = "mcpr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a434a4b9dfa1c5dcef97ee84fdf4c600bab479df9a24259b9e5474e26f6cd47" +dependencies = [ + "anyhow", + "clap", + "log", + "rand 0.8.5", + "reqwest 0.12.9", + "serde", + "serde_json", + "thiserror 1.0.64", + "tiny_http", + "tungstenite", + "url", +] + [[package]] name = "md-5" version = "0.10.5" @@ -5609,6 +5748,7 @@ dependencies = [ "cookie", "cookie_store", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.7", @@ -6879,6 +7019,18 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -7525,6 +7677,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 2db562aa0d..1aa8fcbdb3 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -50,6 +50,7 @@ collab-integrate.workspace = true [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] notify = "6.1.1" +mcpr = "0.2.3" [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.55" diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index be6c743d86..7b26dc1b1d 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -6,6 +6,7 @@ mod chat; mod completion; pub mod entities; mod local_ai; +mod mcp; mod middleware; pub mod notification; mod persistence; diff --git a/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs b/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs new file mode 100644 index 0000000000..80eae6a9bd --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs @@ -0,0 +1,11 @@ +use dashmap::DashMap; +use mcpr::transport::Transport; +use mcpr::{client::Client, transport::sse::SSETransport, transport::stdio::StdioTransport}; +use std::sync::Arc; + +pub struct MCPClientManager { + stdio_clients: Arc>>, + http_client: Arc>>, +} + +impl MCPClientManager {} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs new file mode 100644 index 0000000000..62badbf1f9 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs @@ -0,0 +1 @@ +mod client_manager; From 949556e2fa3be43df6263f373fdbdb2babaa1105 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 12:03:42 +0800 Subject: [PATCH 196/384] chore: remove tauri feature --- frontend/rust-lib/Cargo.lock | 42 ++++++++++++------- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/Cargo.toml | 2 - frontend/rust-lib/flowy-ai/build.rs | 16 ------- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 36 ++++++++++++---- frontend/rust-lib/flowy-core/Cargo.toml | 8 ---- frontend/rust-lib/flowy-database2/Cargo.toml | 1 - frontend/rust-lib/flowy-database2/build.rs | 30 ++++++------- frontend/rust-lib/flowy-date/Cargo.toml | 1 - frontend/rust-lib/flowy-date/build.rs | 16 ------- frontend/rust-lib/flowy-document/Cargo.toml | 5 --- frontend/rust-lib/flowy-document/build.rs | 16 ------- frontend/rust-lib/flowy-error/Cargo.toml | 2 - frontend/rust-lib/flowy-error/build.rs | 14 ------- frontend/rust-lib/flowy-folder/Cargo.toml | 2 - frontend/rust-lib/flowy-folder/build.rs | 16 ------- .../rust-lib/flowy-notification/Cargo.toml | 2 - frontend/rust-lib/flowy-notification/build.rs | 14 ------- frontend/rust-lib/flowy-search/Cargo.toml | 11 +++-- frontend/rust-lib/flowy-search/build.rs | 11 +---- frontend/rust-lib/flowy-storage/Cargo.toml | 1 - frontend/rust-lib/flowy-storage/build.rs | 16 ------- frontend/rust-lib/flowy-user/Cargo.toml | 1 - frontend/rust-lib/flowy-user/build.rs | 16 ------- 24 files changed, 80 insertions(+), 203 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4e1c67233d..6b6af73c8f 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "bytes", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "futures-channel", "futures-util", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "async-trait", @@ -1398,7 +1398,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "bincode", "bytes", @@ -2980,7 +2980,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2995,7 +2995,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "app-error", "jsonwebtoken", @@ -3610,7 +3610,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "bytes", @@ -4647,7 +4647,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4667,6 +4667,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -4734,6 +4735,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6163,7 +6177,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d77dacae32bc25440ca61f675900b80fba6cc9a2#d77dacae32bc25440ca61f675900b80fba6cc9a2" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6b524c41fd..a660ebcb5e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d77dacae32bc25440ca61f675900b80fba6cc9a2" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d77dacae32bc25440ca61f675900b80fba6cc9a2" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "dfd44f8fb7152ace03eb03e46391cf76266948b9" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "dfd44f8fb7152ace03eb03e46391cf76266948b9" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 2db562aa0d..6564b967e4 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -67,5 +67,3 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] -web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] diff --git a/frontend/rust-lib/flowy-ai/build.rs b/frontend/rust-lib/flowy-ai/build.rs index e9230d3d6d..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-ai/build.rs +++ b/frontend/rust-lib/flowy-ai/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 535a3bf8c8..a904bd2c7b 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -55,6 +55,20 @@ pub trait AIExternalService: Send + Sync + 'static { async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError>; } +struct ServerModelsCache { + models: Vec, + timestamp: Option, +} + +impl Default for ServerModelsCache { + fn default() -> Self { + Self { + models: Vec::new(), + timestamp: None, + } + } +} + pub struct AIManager { pub cloud_service_wm: Arc, pub user_service: Arc, @@ -62,7 +76,7 @@ pub struct AIManager { chats: Arc>>, pub local_ai: Arc, pub store_preferences: Arc, - server_models: Arc>>, + server_models: Arc>, } impl AIManager { @@ -270,16 +284,22 @@ impl AIManager { pub async fn get_server_available_models(&self) -> FlowyResult> { let workspace_id = self.user_service.workspace_id()?; + let now = timestamp(); - // First, try reading from the cache. + // First, try reading from the cache with expiration check { let cached_models = self.server_models.read().await; - if !cached_models.is_empty() { - return Ok(cached_models.clone()); + if !cached_models.models.is_empty() { + if let Some(timestamp) = cached_models.timestamp { + // Cache is valid if less than 5 minutes (300 seconds) old + if now - timestamp < 300 { + return Ok(cached_models.models.clone()); + } + } } } - // Cache miss: fetch from the cloud. + // Cache miss or expired: fetch from the cloud. let list = self .cloud_service_wm .get_available_models(&workspace_id) @@ -290,8 +310,10 @@ impl AIManager { .map(|m| m.name) .collect::>(); - // Update the cache. - *self.server_models.write().await = models.clone(); + // Update the cache with new timestamp + let mut cache = self.server_models.write().await; + cache.models = models.clone(); + cache.timestamp = Some(now); Ok(models) } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 7977c33d4f..8c33996046 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -74,14 +74,6 @@ dart = [ "flowy-ai/dart", "flowy-storage/dart", ] -#ts = [ -# "flowy-user/tauri_ts", -# "flowy-folder/tauri_ts", -# "flowy-search/tauri_ts", -# "flowy-database2/ts", -# "flowy-ai/tauri_ts", -# "flowy-storage/tauri_ts", -#] openssl_vendored = ["flowy-sqlite/openssl_vendored"] # Enable/Disable AppFlowy Verbose Log Configuration diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index a6b676512d..d7bee42420 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -62,5 +62,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] verbose_log = ["collab-database/verbose_log"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs index aeaaee42f3..e10aed7956 100644 --- a/frontend/rust-lib/flowy-database2/build.rs +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -5,19 +5,19 @@ fn main() { flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - #[cfg(feature = "ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } + // #[cfg(feature = "ts")] + // { + // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + // flowy_codegen::protobuf_file::ts_gen( + // env!("CARGO_PKG_NAME"), + // env!("CARGO_PKG_NAME"), + // flowy_codegen::Project::Tauri, + // ); + // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + // flowy_codegen::protobuf_file::ts_gen( + // env!("CARGO_PKG_NAME"), + // env!("CARGO_PKG_NAME"), + // flowy_codegen::Project::TauriApp, + // ); + // } } diff --git a/frontend/rust-lib/flowy-date/Cargo.toml b/frontend/rust-lib/flowy-date/Cargo.toml index 40015cad77..d04dfd8416 100644 --- a/frontend/rust-lib/flowy-date/Cargo.toml +++ b/frontend/rust-lib/flowy-date/Cargo.toml @@ -24,4 +24,3 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] -tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-date/build.rs b/frontend/rust-lib/flowy-date/build.rs index e015eb2580..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-date/build.rs +++ b/frontend/rust-lib/flowy-date/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index 77aa321d3c..aaaef4938e 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -50,10 +50,5 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] -tauri_ts = ["flowy-codegen/ts"] -web_ts = [ - "flowy-codegen/ts", -] - # search "Enable/Disable AppFlowy Verbose Log" to find the place that can enable verbose log verbose_log = ["collab-document/verbose_log"] diff --git a/frontend/rust-lib/flowy-document/build.rs b/frontend/rust-lib/flowy-document/build.rs index e015eb2580..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-document/build.rs +++ b/frontend/rust-lib/flowy-document/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index d521e26f4d..7502370a0f 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -54,8 +54,6 @@ impl_from_tantivy = ["tantivy"] impl_from_sqlite = ["flowy-sqlite", "r2d2"] impl_from_appflowy_cloud = ["client-api"] dart = ["flowy-codegen/dart"] -tauri_ts = ["flowy-codegen/ts"] -web_ts = ["flowy-codegen/ts"] [build-dependencies] flowy-codegen = { workspace = true, features = ["proto_gen"] } diff --git a/frontend/rust-lib/flowy-error/build.rs b/frontend/rust-lib/flowy-error/build.rs index 81f0556ae3..8dfda67156 100644 --- a/frontend/rust-lib/flowy-error/build.rs +++ b/frontend/rust-lib/flowy-error/build.rs @@ -1,18 +1,4 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 1a36ecce2c..998fcb84f5 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -50,6 +50,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] -web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] test_helper = [] diff --git a/frontend/rust-lib/flowy-folder/build.rs b/frontend/rust-lib/flowy-folder/build.rs index e9230d3d6d..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-folder/build.rs +++ b/frontend/rust-lib/flowy-folder/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-notification/Cargo.toml b/frontend/rust-lib/flowy-notification/Cargo.toml index b7a96898ff..3851546541 100644 --- a/frontend/rust-lib/flowy-notification/Cargo.toml +++ b/frontend/rust-lib/flowy-notification/Cargo.toml @@ -25,5 +25,3 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] -tauri_ts = ["flowy-codegen/ts"] -web_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-notification/build.rs b/frontend/rust-lib/flowy-notification/build.rs index 81f0556ae3..8dfda67156 100644 --- a/frontend/rust-lib/flowy-notification/build.rs +++ b/frontend/rust-lib/flowy-notification/build.rs @@ -1,18 +1,4 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index 2769f55479..f4d6207783 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -11,11 +11,11 @@ collab-folder = { workspace = true } flowy-derive.workspace = true flowy-error = { workspace = true, features = [ - "impl_from_sqlite", - "impl_from_dispatch_error", - "impl_from_collab_document", - "impl_from_tantivy", - "impl_from_serde", + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_collab_document", + "impl_from_tantivy", + "impl_from_serde", ] } flowy-notification.workspace = true flowy-sqlite.workspace = true @@ -52,4 +52,3 @@ tempfile = "3.10.0" [features] dart = ["flowy-codegen/dart"] -tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-search/build.rs b/frontend/rust-lib/flowy-search/build.rs index 2600d32fb7..482ac05272 100644 --- a/frontend/rust-lib/flowy-search/build.rs +++ b/frontend/rust-lib/flowy-search/build.rs @@ -1,8 +1,5 @@ -#[cfg(feature = "tauri_ts")] -use flowy_codegen::Project; - fn main() { - #[cfg(any(feature = "dart", feature = "tauri_ts"))] + #[cfg(any(feature = "dart"))] let crate_name = env!("CARGO_PKG_NAME"); #[cfg(feature = "dart")] @@ -10,10 +7,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(crate_name); flowy_codegen::dart_event::gen(crate_name); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::protobuf_file::ts_gen(crate_name, crate_name, Project::Tauri); - flowy_codegen::ts_event::gen(crate_name, Project::Tauri); - } } diff --git a/frontend/rust-lib/flowy-storage/Cargo.toml b/frontend/rust-lib/flowy-storage/Cargo.toml index 405faed1ba..c57cfde484 100644 --- a/frontend/rust-lib/flowy-storage/Cargo.toml +++ b/frontend/rust-lib/flowy-storage/Cargo.toml @@ -36,7 +36,6 @@ rand = { version = "0.8", features = ["std_rng"] } [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] [build-dependencies] flowy-codegen.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-storage/build.rs b/frontend/rust-lib/flowy-storage/build.rs index e015eb2580..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-storage/build.rs +++ b/frontend/rust-lib/flowy-storage/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 2a02043f38..5ad579a0fb 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -60,7 +60,6 @@ quickcheck_macros = "1.0" [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-user/build.rs b/frontend/rust-lib/flowy-user/build.rs index e015eb2580..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-user/build.rs +++ b/frontend/rust-lib/flowy-user/build.rs @@ -4,20 +4,4 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - - #[cfg(feature = "tauri_ts")] - { - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); - flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::TauriApp, - ); - } } From 2cbcb320fe706aca1d45a399cf3428f82dd7cdeb Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 12:11:12 +0800 Subject: [PATCH 197/384] chore: add timeout --- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 77 ++++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a904bd2c7b..81ab36bed6 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -284,37 +284,70 @@ impl AIManager { pub async fn get_server_available_models(&self) -> FlowyResult> { let workspace_id = self.user_service.workspace_id()?; - let now = timestamp(); + + let now = timestamp(); // This is safer than using SystemTime which could fail // First, try reading from the cache with expiration check - { + let should_fetch = { let cached_models = self.server_models.read().await; - if !cached_models.models.is_empty() { - if let Some(timestamp) = cached_models.timestamp { - // Cache is valid if less than 5 minutes (300 seconds) old - if now - timestamp < 300 { - return Ok(cached_models.models.clone()); - } - } - } + cached_models.models.is_empty() || cached_models.timestamp.map_or(true, |ts| now - ts >= 300) + }; + + if !should_fetch { + // Cache is still valid, return cached data + let cached_models = self.server_models.read().await; + return Ok(cached_models.models.clone()); } // Cache miss or expired: fetch from the cloud. - let list = self + match self .cloud_service_wm .get_available_models(&workspace_id) - .await?; - let models = list - .models - .into_iter() - .map(|m| m.name) - .collect::>(); + .await + { + Ok(list) => { + let models = list + .models + .into_iter() + .map(|m| m.name) + .collect::>(); - // Update the cache with new timestamp - let mut cache = self.server_models.write().await; - cache.models = models.clone(); - cache.timestamp = Some(now); - Ok(models) + // Update the cache with new timestamp - handle potential errors + if let Err(err) = self.update_models_cache(&models, now).await { + error!("Failed to update models cache: {}", err); + // Still return the fetched models even if caching failed + } + + Ok(models) + }, + Err(err) => { + error!("Failed to fetch available models: {}", err); + + // Return cached data if available, even if expired + let cached_models = self.server_models.read().await; + if !cached_models.models.is_empty() { + info!("Returning expired cached models due to fetch failure"); + return Ok(cached_models.models.clone()); + } + + // If no cached data, return empty list + Ok(Vec::new()) + }, + } + } + + async fn update_models_cache(&self, models: &[String], timestamp: i64) -> FlowyResult<()> { + match self.server_models.try_write() { + Ok(mut cache) => { + cache.models = models.to_vec(); + cache.timestamp = Some(timestamp); + Ok(()) + }, + Err(_) => { + // Handle lock acquisition failure + Err(FlowyError::internal().with_context("Failed to acquire write lock for models cache")) + }, + } } pub async fn update_selected_model(&self, source: String, model: AIModelPB) -> FlowyResult<()> { From 37085042f8a818575657fc96a6ea67f9fc652eb0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 12:40:29 +0800 Subject: [PATCH 198/384] chore: clippy --- .../build-tool/flowy-codegen/src/protobuf_file/mod.rs | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 10 +--------- frontend/rust-lib/flowy-search/build.rs | 7 ++----- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs index 1ddb1bdeb0..8baa78c096 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs @@ -77,7 +77,7 @@ pub fn dart_gen(crate_name: &str) { } #[allow(unused_variables)] -pub fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { +fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { // 1. generate the proto files to proto_file_dir #[cfg(feature = "proto_gen")] let proto_crates = gen_proto_files(crate_name); diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 81ab36bed6..e1e6a2724a 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -55,20 +55,12 @@ pub trait AIExternalService: Send + Sync + 'static { async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError>; } +#[derive(Debug, Default)] struct ServerModelsCache { models: Vec, timestamp: Option, } -impl Default for ServerModelsCache { - fn default() -> Self { - Self { - models: Vec::new(), - timestamp: None, - } - } -} - pub struct AIManager { pub cloud_service_wm: Arc, pub user_service: Arc, diff --git a/frontend/rust-lib/flowy-search/build.rs b/frontend/rust-lib/flowy-search/build.rs index 482ac05272..77c0c8125b 100644 --- a/frontend/rust-lib/flowy-search/build.rs +++ b/frontend/rust-lib/flowy-search/build.rs @@ -1,10 +1,7 @@ fn main() { - #[cfg(any(feature = "dart"))] - let crate_name = env!("CARGO_PKG_NAME"); - #[cfg(feature = "dart")] { - flowy_codegen::protobuf_file::dart_gen(crate_name); - flowy_codegen::dart_event::gen(crate_name); + flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } } From dfb5a6629f8bd33777d589c77262cbd12ba42179 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:27:31 +0800 Subject: [PATCH 199/384] chore: pass ai writer context (#7596) * chore: pass ai writer context * chore: maintain selection after starting ai writer * chore: improve ui of additional comments * chore: revert podfile.lock changes * chore: code readability * chore: revert podfile.lock changes * fix: accept shouldn't try to unformat --- .../desktop_prompt_text_field.dart | 30 +- .../lib/plugins/ai_chat/chat_page.dart | 16 +- .../chat_input/mobile_chat_input.dart | 4 +- .../document/presentation/editor_page.dart | 2 +- .../ai/ai_writer_block_component.dart | 213 +++++++------ .../ai_writer_block_operations.dart | 22 +- .../ai/operations/ai_writer_cubit.dart | 298 +++++++++++------- .../ai/operations/ai_writer_entities.dart | 13 +- .../ai_writer_prompt_input_more_button.dart | 20 +- .../ai/widgets/ai_writer_scroll_wrapper.dart | 31 +- .../base/markdown_text_robot.dart | 2 +- 11 files changed, 388 insertions(+), 263 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 2cd74b5944..0fbdeafee4 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -20,6 +20,7 @@ class DesktopPromptInput extends StatefulWidget { const DesktopPromptInput({ super.key, required this.isStreaming, + required this.textController, required this.onStopStreaming, required this.onSubmitted, required this.selectedSourcesNotifier, @@ -29,6 +30,7 @@ class DesktopPromptInput extends StatefulWidget { }); final bool isStreaming; + final TextEditingController textController; final void Function() onStopStreaming; final void Function(String, PredefinedFormat?, Map) onSubmitted; @@ -47,7 +49,6 @@ class _DesktopPromptInputState extends State { final overlayController = OverlayPortalController(); final inputControlCubit = ChatInputControlCubit(); final focusNode = FocusNode(); - final textController = TextEditingController(); late SendButtonState sendButtonState; bool isComposing = false; @@ -56,7 +57,7 @@ class _DesktopPromptInputState extends State { void initState() { super.initState(); - textController.addListener(handleTextControllerChanged); + widget.textController.addListener(handleTextControllerChanged); focusNode.addListener( () { if (!widget.hideDecoration) { @@ -84,7 +85,7 @@ class _DesktopPromptInputState extends State { @override void dispose() { focusNode.dispose(); - textController.dispose(); + widget.textController.removeListener(handleTextControllerChanged); inputControlCubit.close(); super.dispose(); } @@ -109,7 +110,7 @@ class _DesktopPromptInputState extends State { overlayChildBuilder: (context) { return PromptInputMentionPageMenu( anchor: PromptInputAnchor(textFieldKey, layerLink), - textController: textController, + textController: widget.textController, onPageSelected: handlePageSelected, ); }, @@ -224,12 +225,12 @@ class _DesktopPromptInputState extends State { if (!focusNode.hasFocus) { focusNode.requestFocus(); } - textController.text += '@'; + widget.textController.text += '@'; WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { context .read() - .startSearching(textController.value); + .startSearching(widget.textController.value); overlayController.show(); } }); @@ -245,7 +246,7 @@ class _DesktopPromptInputState extends State { void updateSendButtonState() { if (widget.isStreaming) { sendButtonState = SendButtonState.streaming; - } else if (textController.text.trim().isEmpty) { + } else if (widget.textController.text.trim().isEmpty) { sendButtonState = SendButtonState.disabled; } else { sendButtonState = SendButtonState.enabled; @@ -257,9 +258,9 @@ class _DesktopPromptInputState extends State { return; } final trimmedText = inputControlCubit.formatIntputText( - textController.text.trim(), + widget.textController.text.trim(), ); - textController.clear(); + widget.textController.clear(); if (trimmedText.isEmpty) { return; } @@ -282,7 +283,7 @@ class _DesktopPromptInputState extends State { setState(() { // update whether send button is clickable updateSendButtonState(); - isComposing = !textController.value.composing.isCollapsed; + isComposing = !widget.textController.value.composing.isCollapsed; }); if (isComposing) { @@ -300,6 +301,7 @@ class _DesktopPromptInputState extends State { } // handle cases where mention a page is cancelled + final textController = widget.textController; final textSelection = textController.value.selection; final isSelectingMultipleCharacters = !textSelection.isCollapsed; final isCaretBeforeStartOfRange = @@ -348,7 +350,7 @@ class _DesktopPromptInputState extends State { KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { if (event.character == '@') { WidgetsBinding.instance.addPostFrameCallback((_) { - inputControlCubit.startSearching(textController.value); + inputControlCubit.startSearching(widget.textController.value); overlayController.show(); }); } @@ -356,12 +358,12 @@ class _DesktopPromptInputState extends State { } void handlePageSelected(ViewPB view) { - final newText = textController.text.replaceRange( + final newText = widget.textController.text.replaceRange( inputControlCubit.filterStartPosition, inputControlCubit.filterEndPosition, view.id, ); - textController.value = TextEditingValue( + widget.textController.value = TextEditingValue( text: newText, selection: TextSelection.collapsed( offset: inputControlCubit.filterStartPosition + view.id.length, @@ -386,7 +388,7 @@ class _DesktopPromptInputState extends State { key: textFieldKey, editable: state.editable, cubit: inputControlCubit, - textController: textController, + textController: widget.textController, textFieldFocusNode: focusNode, contentPadding: calculateContentPadding(state.showPredefinedFormats), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index a666039ef2..cbc4929f56 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -385,13 +385,26 @@ class _ChatContentPage extends StatelessWidget { } } -class _Input extends StatelessWidget { +class _Input extends StatefulWidget { const _Input({ required this.view, }); final ViewPB view; + @override + State<_Input> createState() => _InputState(); +} + +class _InputState extends State<_Input> { + final textController = TextEditingController(); + + @override + void dispose() { + textController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocSelector( @@ -421,6 +434,7 @@ class _Input extends StatelessWidget { return UniversalPlatform.isDesktop ? DesktopPromptInput( isStreaming: !canSendMessage, + textController: textController, onStopStreaming: () { chatBloc.add(const ChatEvent.stopStream()); }, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart index 0aa7465dfb..76d1af7134 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart @@ -41,7 +41,7 @@ class _MobileChatInputState extends State { void initState() { super.initState(); - textController.addListener(handleTextControllerChange); + textController.addListener(handleTextControllerChanged); // focusNode.onKeyEvent = handleKeyEvent; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -197,7 +197,7 @@ class _MobileChatInputState extends State { ); } - void handleTextControllerChange() { + void handleTextControllerChanged() { if (textController.value.isComposingRangeValid) { return; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index f445e698aa..b14d930d65 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -393,7 +393,7 @@ class _AppFlowyEditorPageState extends State }, child: SizedBox( width: double.infinity, - height: UniversalPlatform.isDesktopOrWeb ? 300 : 400, + height: UniversalPlatform.isDesktopOrWeb ? 600 : 400, ), ), dropTargetStyle: AppFlowyDropTargetStyle( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 1d25723854..a35d0cfbf8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -107,9 +107,7 @@ class _AIWriterBlockComponentState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { overlayController.show(); - if (!widget.node.isAiWriterInitialized) { - context.read().register(widget.node); - } + context.read().register(widget.node); }); } @@ -150,6 +148,7 @@ class _AIWriterBlockComponentState extends State { child: OverlayContent( editorState: editorState, node: widget.node, + textController: textController, ), ), ), @@ -178,10 +177,12 @@ class OverlayContent extends StatefulWidget { super.key, required this.editorState, required this.node, + required this.textController, }); final EditorState editorState; final Node node; + final TextEditingController textController; @override State createState() => _OverlayContentState(); @@ -200,31 +201,40 @@ class _OverlayContentState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (state is IdleAiWriterState) { + if (state is IdleAiWriterState || + state is DocumentContentEmptyAiWriterState) { return const SizedBox.shrink(); } + + final command = (state as RegisteredAiWriter).command; + final selection = widget.node.aiWriterSelection; - final showSuggestionPopup = - state is ReadyAiWriterState && !state.isFirstRun; - final isInitialReadyState = - state is ReadyAiWriterState && state.isFirstRun; + final hasSelection = selection != null && !selection.isCollapsed; + final markdownText = switch (state) { final ReadyAiWriterState ready => ready.markdownText, final GeneratingAiWriterState generating => generating.markdownText, _ => '', }; - final hasSelection = selection != null && !selection.isCollapsed; - final isLightMode = Theme.of(context).isLightMode; - final darkBorderColor = - isLightMode ? Color(0x1F1F2329) : Color(0xFF505469); + final showSuggestedActions = + state is ReadyAiWriterState && !state.isFirstRun; + final isInitialReadyState = + state is ReadyAiWriterState && state.isFirstRun; + final showSuggestedActionsPopup = + showSuggestedActions && markdownText.isEmpty; + final showSuggestedActionsWithin = + showSuggestedActions && markdownText.isNotEmpty; + + final darkBorderColor = Theme.of(context).isLightMode + ? Color(0x1F1F2329) + : Color(0xFF505469); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showSuggestionPopup && - state.command != AiWriterCommand.explain) ...[ + if (showSuggestedActionsPopup) ...[ Container( padding: EdgeInsets.all(4.0), decoration: _getModalDecoration( @@ -234,7 +244,7 @@ class _OverlayContentState extends State { borderColor: darkBorderColor, ), child: SuggestionActionBar( - currentCommand: state.command, + currentCommand: command, hasSelection: hasSelection, onTap: (action) { _onSelectSuggestionAction(context, action); @@ -243,85 +253,40 @@ class _OverlayContentState extends State { ), const VSpace(4.0 + 1.0), ], - DecoratedBox( + Container( decoration: _getModalDecoration( context, color: null, borderColor: darkBorderColor, borderRadius: BorderRadius.all(Radius.circular(12.0)), ), + constraints: BoxConstraints(maxHeight: 400), child: Column( + mainAxisSize: MainAxisSize.min, children: [ if (markdownText.isNotEmpty) ...[ - DecoratedBox( - decoration: _getHelperChildDecoration(context), - child: Container( - constraints: BoxConstraints(maxHeight: 140), - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - physics: ClampingScrollPhysics(), - padding: EdgeInsets.only(top: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 24.0, - padding: - EdgeInsets.symmetric(horizontal: 6.0), - alignment: - AlignmentDirectional.centerStart, - child: FlowyText( - (state as RegisteredAiWriter) - .command - .i18n, - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF666D76), - ), - ), - const VSpace(4.0), - Padding( - padding: - EdgeInsets.symmetric(horizontal: 6.0), - child: AIMarkdownText( - markdown: markdownText, - ), - ), - ], - ), - ), - ), - if (showSuggestionPopup && - state.command == AiWriterCommand.explain) ...[ - const VSpace(4.0), - SuggestionActionBar( - currentCommand: state.command, - hasSelection: hasSelection, - onTap: (action) { - _onSelectSuggestionAction(context, action); - }, - ), - ], - const VSpace(8.0), - ], + Flexible( + child: DecoratedBox( + decoration: _secondaryContentDecoration(context), + child: SecondaryContentArea( + markdownText: markdownText, + onSelectSuggestionAction: (action) { + _onSelectSuggestionAction(context, action); + }, + command: command, + showSuggestionActions: showSuggestedActionsWithin, + hasSelection: hasSelection, ), ), ), - Divider( - height: 1.0, - ), + Divider(height: 1.0), ], DecoratedBox( decoration: markdownText.isNotEmpty - ? _getInputChildDecoration(context) + ? _mainContentDecoration(context) : _getSingleChildDeocoration(context), child: MainContentArea( + textController: widget.textController, isDocumentEmpty: _isDocumentEmpty(), isInitialReadyState: isInitialReadyState, showCommandsToggle: showCommandsToggle, @@ -338,21 +303,19 @@ class _OverlayContentState extends State { } return Align( alignment: AlignmentDirectional.centerEnd, - child: BottomCommandButtons( + child: MoreAiWriterCommands( hasSelection: hasSelection, editorState: widget.editorState, onSelectCommand: (command) { - final promptInputBloc = context.read(); - final showPredefinedFormats = - promptInputBloc.state.showPredefinedFormats; - final predefinedFormat = - promptInputBloc.state.predefinedFormat; + final state = context.read().state; + final showPredefinedFormats = state.showPredefinedFormats; + final predefinedFormat = state.predefinedFormat; + final text = widget.textController.text; context.read().runCommand( command, - predefinedFormat: - showPredefinedFormats ? predefinedFormat : null, - isFirstRun: true, + text, + showPredefinedFormats ? predefinedFormat : null, ); }, ), @@ -395,14 +358,14 @@ class _OverlayContentState extends State { ); } - BoxDecoration _getHelperChildDecoration(BuildContext context) { + BoxDecoration _secondaryContentDecoration(BuildContext context) { return BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)), ); } - BoxDecoration _getInputChildDecoration(BuildContext context) { + BoxDecoration _mainContentDecoration(BuildContext context) { return BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.vertical(bottom: Radius.circular(12.0)), @@ -436,14 +399,83 @@ class _OverlayContentState extends State { } } +class SecondaryContentArea extends StatelessWidget { + const SecondaryContentArea({ + super.key, + required this.command, + required this.markdownText, + required this.showSuggestionActions, + required this.hasSelection, + required this.onSelectSuggestionAction, + }); + + final AiWriterCommand command; + final String markdownText; + final bool showSuggestionActions; + final bool hasSelection; + final void Function(SuggestionAction) onSelectSuggestionAction; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: SingleChildScrollView( + physics: ClampingScrollPhysics(), + padding: EdgeInsets.only(top: 8.0, left: 14.0, right: 14.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 24.0, + alignment: AlignmentDirectional.centerStart, + child: FlowyText( + command.i18n, + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF666D76), + ), + ), + const VSpace(4.0), + AIMarkdownText( + markdown: markdownText, + ), + ], + ), + ), + ), + if (showSuggestionActions) ...[ + const VSpace(4.0), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: SuggestionActionBar( + currentCommand: command, + hasSelection: hasSelection, + onTap: onSelectSuggestionAction, + ), + ), + ], + const VSpace(8.0), + ], + ), + ); + } +} + class MainContentArea extends StatelessWidget { const MainContentArea({ super.key, + required this.textController, required this.isInitialReadyState, required this.isDocumentEmpty, required this.showCommandsToggle, }); + final TextEditingController textController; final bool isInitialReadyState; final bool isDocumentEmpty; final ValueNotifier showCommandsToggle; @@ -458,7 +490,10 @@ class MainContentArea extends StatelessWidget { return DesktopPromptInput( isStreaming: false, hideDecoration: true, - onSubmitted: (message, format, _) => cubit.submit(message, format), + textController: textController, + onSubmitted: (message, format, _) { + cubit.runCommand(state.command, message, format); + }, onStopStreaming: () => cubit.stopStream(), selectedSourcesNotifier: cubit.selectedSourcesNotifier, onUpdateSelectedSources: (sources) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart index c503042339..d48c655e56 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart @@ -1,8 +1,11 @@ +import 'dart:async'; + import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import '../ai_writer_block_component.dart'; import 'ai_writer_entities.dart'; +import 'ai_writer_node_extension.dart'; Future setAiWriterNodeIsInitialized( EditorState editorState, @@ -11,13 +14,26 @@ Future setAiWriterNodeIsInitialized( final transaction = editorState.transaction ..updateNode(node, { AiWriterBlockKeys.isInitialized: true, - }) - ..afterSelection = null; + }); await editorState.apply( transaction, - options: const ApplyOptions(recordUndo: false), + options: const ApplyOptions( + recordUndo: false, + inMemoryUpdate: true, + ), + withUpdateSelection: false, ); + + final selection = node.aiWriterSelection; + if (selection != null && !selection.isCollapsed) { + unawaited( + editorState.updateSelectionWithReason( + selection, + extraInfo: {selectionExtraInfoDisableToolbar: true}, + ), + ); + } } Future removeAiWriterNode( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 3b0e04e828..079a3a2749 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import '../../base/markdown_text_robot.dart'; @@ -41,8 +40,6 @@ class AiWriterCubit extends Cubit { final List records = []; final ValueNotifier> selectedSourcesNotifier; - (String, PredefinedFormat?)? _previousPrompt; - bool acceptReplacesOriginal = false; @override Future close() async { @@ -50,31 +47,20 @@ class AiWriterCubit extends Cubit { await super.close(); } - void register(Node node) async { - if (aiWriterNode != null && node.id != aiWriterNode!.id) { - await removeAiWriterNode(editorState, node); - return; + Future exit({ + bool withDiscard = true, + bool withUnformat = true, + }) async { + if (withDiscard) { + await _textRobot.discard(); } - aiWriterNode = node; - onCreateNode?.call(); - - await setAiWriterNodeIsInitialized(editorState, node); - - final command = node.aiWriterCommand; - if (command == AiWriterCommand.userQuestion) { - emit(ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true)); - } else { - runCommand(command, isFirstRun: true); - } - } - - Future exit() async { - await _textRobot.discard(); _textRobot.reset(); onRemoveNode?.call(); + records.clear(); + selectedSourcesNotifier.value = [documentId]; emit(IdleAiWriterState()); - if (aiWriterNode != null) { + if (withUnformat) { final selection = aiWriterNode!.aiWriterSelection; if (selection == null) { return; @@ -94,43 +80,96 @@ class AiWriterCubit extends Cubit { ), withUpdateSelection: false, ); + } + if (aiWriterNode != null) { await removeAiWriterNode(editorState, aiWriterNode!); aiWriterNode = null; } } + void register(Node node) async { + if (node.isAiWriterInitialized) { + return; + } + if (aiWriterNode != null && node.id != aiWriterNode!.id) { + await removeAiWriterNode(editorState, node); + return; + } + + aiWriterNode = node; + onCreateNode?.call(); + + await setAiWriterNodeIsInitialized(editorState, node); + + final command = node.aiWriterCommand; + final (run, prompt) = await _addSelectionTextToRecords(command); + + if (!run) { + await exit(); + return; + } + + runCommand(command, prompt, null); + } + void runCommand( - AiWriterCommand command, { - required bool isFirstRun, + AiWriterCommand command, + String prompt, PredefinedFormat? predefinedFormat, - bool isRetry = false, - }) async { + ) async { + if (aiWriterNode == null) { + return; + } + switch (command) { case AiWriterCommand.continueWriting: await _startContinueWriting( command, predefinedFormat, - isImmediateRun: isFirstRun, ); break; case AiWriterCommand.fixSpellingAndGrammar: case AiWriterCommand.improveWriting: case AiWriterCommand.makeLonger: case AiWriterCommand.makeShorter: - await _startSuggestingEdits(command, predefinedFormat); + await _startSuggestingEdits(command, prompt, predefinedFormat); break; case AiWriterCommand.explain: - await _startInforming(command, predefinedFormat); + await _startInforming(command, prompt, predefinedFormat); + break; + case AiWriterCommand.userQuestion when prompt.isNotEmpty: + _startAskingQuestion(prompt, predefinedFormat); break; case AiWriterCommand.userQuestion: - if (isRetry && _previousPrompt != null) { - submit(_previousPrompt!.$1, _previousPrompt!.$2); - } + emit( + ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true), + ); break; } } + void _retry({ + required PredefinedFormat? predefinedFormat, + }) async { + final lastQuestion = + records.lastWhereOrNull((record) => record.role == AiRole.user); + + if (lastQuestion != null && state is RegisteredAiWriter) { + await _textRobot.discard(); + _textRobot.reset(); + runCommand( + (state as RegisteredAiWriter).command, + lastQuestion.content, + lastQuestion.format, + ); + } + } + Future stopStream() async { + if (aiWriterNode == null) { + return; + } + if (state is GeneratingAiWriterState) { final generatingState = state as GeneratingAiWriterState; @@ -138,6 +177,10 @@ class AiWriterCubit extends Cubit { attributes: ApplySuggestionFormatType.replace.attributes, ); + if (_textRobot.hasAnyResult) { + records.add(AiWriterRecord.ai(content: _textRobot.markdownText)); + } + await AIEventStopCompleteText( CompleteTextTaskPB( taskId: generatingState.taskId, @@ -162,34 +205,20 @@ class AiWriterCubit extends Cubit { return; } - if (state is! RegisteredAiWriter) { - return; - } - - final command = (state as RegisteredAiWriter).command; - if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { - await _textRobot.discard(); - _textRobot.reset(); - runCommand( - command, - predefinedFormat: predefinedFormat, - isRetry: true, - isFirstRun: false, - ); + _retry(predefinedFormat: predefinedFormat); return; } - - final selection = aiWriterNode!.aiWriterSelection; - if (selection == null) { - return; - } - if (action case SuggestionAction.discard || SuggestionAction.close) { await exit(); return; } + final selection = aiWriterNode?.aiWriterSelection; + if (selection == null) { + return; + } + if (action case SuggestionAction.accept) { await _textRobot.persist(); final nodes = editorState.getNodesInSelection(selection); @@ -199,6 +228,8 @@ class AiWriterCubit extends Cubit { options: const ApplyOptions(recordUndo: false), withUpdateSelection: false, ); + await exit(withDiscard: false, withUnformat: false); + return; } if (action case SuggestionAction.keep) { @@ -206,8 +237,12 @@ class AiWriterCubit extends Cubit { } if (action case SuggestionAction.insertBelow) { - if (state case final ReadyAiWriterState readyState - when readyState.markdownText.isNotEmpty) { + if (state is! ReadyAiWriterState) { + return; + } + final command = (state as ReadyAiWriterState).command; + final markdownText = (state as ReadyAiWriterState).markdownText; + if (command == AiWriterCommand.explain && markdownText.isNotEmpty) { final transaction = editorState.transaction; final position = ensurePreviousNodeIsEmptyParagraph( editorState, @@ -224,8 +259,8 @@ class AiWriterCubit extends Cubit { withUpdateSelection: false, ); _textRobot.start(position: position); - await _textRobot.persist(markdownText: readyState.markdownText); - } else { + await _textRobot.persist(markdownText: markdownText); + } else if (_textRobot.hasAnyResult) { await _textRobot.persist(); } @@ -243,9 +278,7 @@ class AiWriterCubit extends Cubit { ); } - await removeAiWriterNode(editorState, aiWriterNode!); - aiWriterNode = null; - emit(IdleAiWriterState()); + await exit(withDiscard: false); } bool hasUnusedResponse() { @@ -260,7 +293,56 @@ class AiWriterCubit extends Cubit { }; } - void submit( + Future<(bool, String)> _addSelectionTextToRecords( + AiWriterCommand command, + ) async { + final node = aiWriterNode; + if (node == null) { + return (false, ''); + } + final selection = node.aiWriterSelection?.normalized; + if (selection == null) { + return (false, ''); + } + + if (command == AiWriterCommand.continueWriting) { + return (true, ''); + } else { + if (selection.isCollapsed) { + return (true, ''); + } else { + final selectionText = + await editorState.getMarkdownInSelection(selection); + + if (command == AiWriterCommand.userQuestion) { + records.add( + AiWriterRecord.user(content: selectionText, format: null), + ); + return (true, ''); + } else { + return (true, selectionText); + } + } + } + } + + Future _getDocumentContentFromTopToPosition(Position position) async { + final beginningToCursorSelection = Selection( + start: Position(path: [0]), + end: position, + ).normalized; + + final documentText = + (await editorState.getMarkdownInSelection(beginningToCursorSelection)) + .trim(); + + final view = await ViewBackendService.getView(documentId).toNullable(); + final viewName = view?.name ?? ''; + + return "$viewName\n$documentText".trim(); + } + + void _startAskingQuestion( String prompt, PredefinedFormat? format, ) async { @@ -268,7 +350,6 @@ class AiWriterCubit extends Cubit { return; } final command = AiWriterCommand.userQuestion; - _previousPrompt = (prompt, format); final stream = await _aiService.streamCompletion( objectId: documentId, @@ -294,7 +375,10 @@ class AiWriterCubit extends Cubit { ); _textRobot.start(position: position); records.add( - AiWriterRecord.user(content: prompt), + AiWriterRecord.user( + content: prompt, + format: format, + ), ); }, processMessage: (text) async { @@ -353,43 +437,19 @@ class AiWriterCubit extends Cubit { Future _startContinueWriting( AiWriterCommand command, - PredefinedFormat? predefinedFormat, { - required bool isImmediateRun, - }) async { - if (aiWriterNode == null) { + PredefinedFormat? predefinedFormat, + ) async { + final position = aiWriterNode?.aiWriterSelection?.start; + if (position == null) { return; } - final cursorPosition = aiWriterNode?.aiWriterSelection?.start; - if (cursorPosition == null) { - return; - } - final selection = Selection( - start: Position(path: [0]), - end: cursorPosition, - ).normalized; + final text = await _getDocumentContentFromTopToPosition(position); - String text = (await editorState.getMarkdownInSelection(selection)).trim(); if (text.isEmpty) { - final view = await ViewBackendService.getView(documentId).toNullable(); - if (view == null || - view.name.isEmpty || - view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) { - final stateCopy = state; - emit( - DocumentContentEmptyAiWriterState( - command, - onConfirm: () { - if (isImmediateRun) { - removeAiWriterNode(editorState, aiWriterNode!); - } - }, - ), - ); - emit(stateCopy); - return; - } else { - text = view.name; - } + final stateCopy = state; + emit(DocumentContentEmptyAiWriterState(command, onConfirm: exit)); + emit(stateCopy); + return; } final stream = await _aiService.streamCompletion( @@ -397,6 +457,7 @@ class AiWriterCubit extends Cubit { text: text, completionType: command.toCompletionType(), history: records, + sourceIds: selectedSourcesNotifier.value, onStart: () async { final transaction = editorState.transaction; final position = ensurePreviousNodeIsEmptyParagraph( @@ -414,6 +475,12 @@ class AiWriterCubit extends Cubit { withUpdateSelection: false, ); _textRobot.start(position: position); + records.add( + AiWriterRecord.user( + content: text, + format: predefinedFormat, + ), + ); }, processMessage: (text) async { await _textRobot.appendMarkdownText( @@ -467,23 +534,23 @@ class AiWriterCubit extends Cubit { Future _startSuggestingEdits( AiWriterCommand command, + String prompt, PredefinedFormat? predefinedFormat, ) async { - if (aiWriterNode == null) { - return; - } final selection = aiWriterNode?.aiWriterSelection; if (selection == null) { return; } - - acceptReplacesOriginal = true; + if (prompt.isEmpty) { + prompt = records.removeAt(0).content; + } final stream = await _aiService.streamCompletion( objectId: documentId, - text: await editorState.getMarkdownInSelection(selection), + text: prompt, completionType: command.toCompletionType(), history: records, + sourceIds: selectedSourcesNotifier.value, onStart: () async { final transaction = editorState.transaction; formatSelection( @@ -507,6 +574,12 @@ class AiWriterCubit extends Cubit { withUpdateSelection: false, ); _textRobot.start(position: position); + records.add( + AiWriterRecord.user( + content: prompt, + format: predefinedFormat, + ), + ); }, processMessage: (text) async { await _textRobot.appendMarkdownText( @@ -560,22 +633,31 @@ class AiWriterCubit extends Cubit { Future _startInforming( AiWriterCommand command, + String prompt, PredefinedFormat? predefinedFormat, ) async { - if (aiWriterNode == null) { - return; - } final selection = aiWriterNode?.aiWriterSelection; if (selection == null) { return; } + if (prompt.isEmpty) { + prompt = records.removeAt(0).content; + } final stream = await _aiService.streamCompletion( objectId: documentId, - text: await editorState.getMarkdownInSelection(selection), + text: prompt, completionType: command.toCompletionType(), history: records, - onStart: () async {}, + sourceIds: selectedSourcesNotifier.value, + onStart: () async { + records.add( + AiWriterRecord.user( + content: prompt, + format: predefinedFormat, + ), + ); + }, processMessage: (text) async { if (state case final GeneratingAiWriterState generatingState) { emit( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart index 7dc8ffc04e..f15c2e6d7f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; @@ -129,24 +130,22 @@ enum AiRole { } class AiWriterRecord extends Equatable { - const AiWriterRecord({ - required this.role, - required this.content, - }); - const AiWriterRecord.user({ required this.content, + required this.format, }) : role = AiRole.user; const AiWriterRecord.ai({ required this.content, - }) : role = AiRole.ai; + }) : role = AiRole.ai, + format = null; final AiRole role; final String content; + final PredefinedFormat? format; @override - List get props => [role, content]; + List get props => [role, content, format]; CompletionRecordPB toPB() { return CompletionRecordPB( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart index 1e2641d796..eefb122a20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart @@ -1,14 +1,12 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/colorscheme/default_colorscheme.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 '../operations/ai_writer_entities.dart'; @@ -74,8 +72,8 @@ class AiWriterPromptMoreButton extends StatelessWidget { } } -class BottomCommandButtons extends StatelessWidget { - const BottomCommandButtons({ +class MoreAiWriterCommands extends StatelessWidget { + const MoreAiWriterCommands({ super.key, required this.hasSelection, required this.editorState, @@ -155,19 +153,7 @@ class BottomCommandButtons extends StatelessWidget { command.i18n, figmaLineHeight: 20, ), - onTap: () { - final aiInputBloc = context.read(); - final showPredefinedFormats = - aiInputBloc.state.showPredefinedFormats; - final predefinedFormat = aiInputBloc.state.predefinedFormat; - - context.read().runCommand( - command, - predefinedFormat: - showPredefinedFormats ? predefinedFormat : null, - isFirstRun: false, - ); - }, + onTap: () => onSelectCommand(command), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 4c04baf557..8a054de1c0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -145,19 +145,11 @@ class _AiWriterScrollWrapperState extends State { description: LocaleKeys.document_plugins_discardResponse.tr(), confirmLabel: LocaleKeys.button_discard.tr(), style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); - }, + onConfirm: stopAndExit, onCancel: () {}, ); } else { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); + stopAndExit(); } } @@ -177,19 +169,11 @@ class _AiWriterScrollWrapperState extends State { description: LocaleKeys.document_plugins_discardResponse.tr(), confirmLabel: LocaleKeys.button_discard.tr(), style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); - }, + onConfirm: stopAndExit, onCancel: () {}, ); } else { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); + stopAndExit(); } return true; case LogicalKeyboardKey.keyC @@ -220,4 +204,11 @@ class _AiWriterScrollWrapperState extends State { } }); } + + void stopAndExit() { + Future(() async { + await aiWriterCubit.stopStream(); + await aiWriterCubit.exit(); + }); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 1904b10934..2eafb0af7c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -140,7 +140,7 @@ class MarkdownTextRobot { await editorState.apply( transaction, - options: const ApplyOptions(recordUndo: false), + options: const ApplyOptions(recordUndo: false, inMemoryUpdate: true), ); if (_enableDebug) { From 682a50da533739a07550d21656e8ec140f217dfb Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:15:20 +0800 Subject: [PATCH 200/384] chore: replace ai response every time (#7597) --- .../editor_plugins/ai/operations/ai_writer_cubit.dart | 5 +++-- .../editor_plugins/base/markdown_text_robot.dart | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 079a3a2749..0784ade2cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -121,6 +121,9 @@ class AiWriterCubit extends Cubit { return; } + await _textRobot.discard(); + _textRobot.reset(); + switch (command) { case AiWriterCommand.continueWriting: await _startContinueWriting( @@ -155,8 +158,6 @@ class AiWriterCubit extends Cubit { records.lastWhereOrNull((record) => record.role == AiRole.user); if (lastQuestion != null && state is RegisteredAiWriter) { - await _textRobot.discard(); - _textRobot.reset(); runCommand( (state as RegisteredAiWriter).command, lastQuestion.content, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 2eafb0af7c..62c024b31a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -151,7 +151,6 @@ class MarkdownTextRobot { void reset() { _markdownText = ''; _insertedNodes = []; - _insertPosition = null; } Future _refresh({ From 66ce786726eed1fb06a4e6281f7b13a4267e19e9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 16:38:37 +0800 Subject: [PATCH 201/384] chore: add client --- .../flowy-ai/src/mcp/client_manager.rs | 141 +++++++++++++++++- frontend/rust-lib/flowy-error/src/code.rs | 3 + 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs b/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs index 80eae6a9bd..e48af10071 100644 --- a/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/mcp/client_manager.rs @@ -1,11 +1,144 @@ +use anyhow::Context; use dashmap::DashMap; +use flowy_error::{ErrorCode, FlowyError}; +use mcpr::error::MCPError; +use mcpr::schema::JSONRPCRequest; use mcpr::transport::Transport; use mcpr::{client::Client, transport::sse::SSETransport, transport::stdio::StdioTransport}; +use serde_json::Value; +use std::io::{BufRead, BufReader}; +use std::process::{Child, Command, Stdio}; use std::sync::Arc; +use std::thread; +use tracing::{debug, info}; -pub struct MCPClientManager { - stdio_clients: Arc>>, - http_client: Arc>>, +pub struct MCPServerConfig { + server_cmd: String, + args: Vec, } -impl MCPClientManager {} +impl MCPServerConfig { + pub fn is_sse_server(&self) -> bool { + self.server_cmd.starts_with("http") + } +} + +pub struct MCPClient { + client: Client, + process: Option, +} + +impl MCPClient +where + T: Transport, +{ + pub fn initialize(&mut self) -> Result<(), FlowyError> { + self + .client + .initialize() + .map(|err| FlowyError::new(ErrorCode::MCPError).with_context(err))?; + Ok(()) + } +} + +impl Drop for MCPClient { + fn drop(&mut self) { + if let Some(process) = &mut self.process { + let _ = process.kill(); + } + } +} + +pub struct MCPClientManager { + stdio_clients: Arc>>, + sse_clients: Arc>>, +} + +impl MCPClientManager { + pub fn new() -> MCPClientManager { + Self { + stdio_clients: Arc::new(DashMap::new()), + sse_clients: Arc::new(DashMap::new()), + } + } + + pub async fn connect_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { + if config.is_sse_server() { + let context = connect_to_http_server(&config.server_cmd, &config.args).await?; + self.sse_clients.insert(config.server_cmd, context); + } else { + let context = connect_to_stdio_server(&config.server_cmd, &config.args).await?; + self.stdio_clients.insert(config.server_cmd, context); + } + Ok(()) + } + + pub async fn remove_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { + if config.is_sse_server() { + self.sse_clients.remove(&config.server_cmd); + } else { + self.stdio_clients.remove(&config.server_cmd); + } + Ok(()) + } +} + +async fn connect_to_http_server( + command: &str, + args: &[String], +) -> Result, FlowyError> { + info!( + "Connecting to running server with command: {} {}", + command, + args.join(" ") + ); + + let transport = SSETransport::new_server(&command); + let client = Client::new(transport); + Ok(MCPClient { + client, + process: None, + }) +} + +async fn connect_to_stdio_server( + command: &str, + args: &[String], +) -> Result, FlowyError> { + info!( + "Connecting to running server with command: {} {}", + command, + args.join(" ") + ); + + // Start a new process that will connect to the server + let mut process = Command::new(command) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to spawn process")?; + + if let Some(stderr) = process.stderr.take() { + let stderr_reader = BufReader::new(stderr); + thread::spawn(move || { + for line in stderr_reader.lines() { + if let Ok(line) = line { + debug!("Server stderr: {}", line); + } + } + }); + } + + let transport = StdioTransport::with_reader_writer( + Box::new(process.stdout.take().context("Failed to get stdout")?), + Box::new(process.stdin.take().context("Failed to get stdin")?), + ); + + let client = Client::new(transport); + Ok(MCPClient { + client, + process: Some(process), + }) +} diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 28ff1a5a20..b16099013b 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -374,6 +374,9 @@ pub enum ErrorCode { #[error("Local AI is not ready")] LocalAINotReady = 128, + + #[error("MCP error")] + MCPError = 129, } impl ErrorCode { From 7463e4e3eba830caf1cd3ab01ea8dc2e98c38b79 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:50:16 +0800 Subject: [PATCH 202/384] fix: pass response format in ai writer (#7599) --- .../editor_plugins/ai/operations/ai_writer_cubit.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 0784ade2cd..07d5d84ae9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -459,6 +459,7 @@ class AiWriterCubit extends Cubit { completionType: command.toCompletionType(), history: records, sourceIds: selectedSourcesNotifier.value, + format: predefinedFormat, onStart: () async { final transaction = editorState.transaction; final position = ensurePreviousNodeIsEmptyParagraph( @@ -549,6 +550,7 @@ class AiWriterCubit extends Cubit { final stream = await _aiService.streamCompletion( objectId: documentId, text: prompt, + format: predefinedFormat, completionType: command.toCompletionType(), history: records, sourceIds: selectedSourcesNotifier.value, @@ -651,6 +653,7 @@ class AiWriterCubit extends Cubit { completionType: command.toCompletionType(), history: records, sourceIds: selectedSourcesNotifier.value, + format: predefinedFormat, onStart: () async { records.add( AiWriterRecord.user( From 35081fd31133a767a2fa4e1c155b9ee2471e3f0c Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 21:59:42 +0800 Subject: [PATCH 203/384] chore: remove default model name --- .../desktop_prompt_text_field.dart | 2 + .../setting/ai/ai_settings_group.dart | 6 +-- .../settings/ai/settings_ai_bloc.dart | 49 +++++++------------ .../setting_ai_view/model_selection.dart | 4 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++++--------- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 27 +++++----- frontend/rust-lib/flowy-ai/src/entities.rs | 25 +++++++++- .../rust-lib/flowy-ai/src/event_handler.rs | 6 ++- .../flowy-user/src/entities/user_profile.rs | 5 +- 9 files changed, 90 insertions(+), 80 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 0fbdeafee4..ba2d035183 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -739,6 +739,7 @@ class _SelectModelButtonState extends State { ); }, child: _CurrentModelButton( + key: ValueKey(state.availableModels?.selectedModel.name), modelName: state.availableModels?.selectedModel.name ?? "", onTap: () => popoverController.show(), ), @@ -810,6 +811,7 @@ class _CurrentModelButton extends StatelessWidget { const _CurrentModelButton({ required this.modelName, required this.onTap, + super.key, }); final String modelName; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart index f67cc9e6b8..3b75b08543 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart @@ -87,13 +87,13 @@ class AiSettingsGroup extends StatelessWidget { children: availableModels .mapIndexed( (index, model) => FlowyOptionTile.checkbox( - text: model, + text: model.name, showTopBorder: index == 0, - isSelected: state.selectedAIModel == model, + isSelected: state.selectedAIModel == model.name, onTap: () { context .read() - .add(SettingsAIEvent.selectModel(model)); + .add(SettingsAIEvent.selectModel(model.name)); context.pop(); }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 74a61fe96c..6dcd52b294 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -1,9 +1,8 @@ -import 'dart:convert'; - import 'package:appflowy/user/application/user_listener.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-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -95,32 +94,22 @@ class SettingsAIBloc extends Bloc { ), ); }, - didLoadAvailableModels: (String models) { - final dynamic decodedJson = jsonDecode(models); - Log.info("Available models: $decodedJson"); - if (decodedJson is Map) { - final models = ModelList.fromJson(decodedJson).models; - if (models.isEmpty) { - // If available models is empty, then we just show the - // Default - emit(state.copyWith(availableModels: ["Default"])); - return; - } - - if (!models.contains(state.selectedAIModel)) { - // Use first model as default model if current selected model - // is not available - final selectedModel = models[0]; - _updateUserWorkspaceSetting(model: selectedModel); - emit( - state.copyWith( - availableModels: models, - selectedAIModel: selectedModel, - ), - ); - } else { - emit(state.copyWith(availableModels: models)); - } + didLoadAvailableModels: (List models) { + if (state.selectedAIModel.isEmpty) { + final m = models.firstWhere((model) => model.isDefault); + _updateUserWorkspaceSetting(model: m.name); + emit( + state.copyWith( + availableModels: models, + selectedAIModel: m.name, + ), + ); + } else { + emit( + state.copyWith( + availableModels: models, + ), + ); } }, refreshMember: (member) { @@ -203,7 +192,7 @@ class SettingsAIEvent with _$SettingsAIEvent { ) = _DidReceiveUserProfile; const factory SettingsAIEvent.didLoadAvailableModels( - String models, + List models, ) = _DidLoadAvailableModels; } @@ -214,7 +203,7 @@ class SettingsAIState with _$SettingsAIState { UseAISettingPB? aiSettings, @Default("Default") String selectedAIModel, AFRolePB? currentWorkspaceMemberRole, - @Default(["Default"]) List availableModels, + @Default([]) List availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index dfc53e4f08..8cd2f31114 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -38,8 +38,8 @@ class AIModelSelection extends StatelessWidget { .map( (model) => buildDropdownMenuEntry( context, - value: model, - label: model, + value: model.name, + label: model.name, ), ) .toList(), diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index e1e6a2724a..2486637ef8 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -20,6 +20,7 @@ use crate::util::ai_available_models_key; use collab_integrate::persistence::collab_metadata_sql::{ batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata, }; +use flowy_ai_pub::cloud::ai_dto::AvailableModel; use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; @@ -57,7 +58,7 @@ pub trait AIExternalService: Send + Sync + 'static { #[derive(Debug, Default)] struct ServerModelsCache { - models: Vec, + models: Vec, timestamp: Option, } @@ -274,10 +275,9 @@ impl AIManager { Ok(model) } - pub async fn get_server_available_models(&self) -> FlowyResult> { + pub async fn get_server_available_models(&self) -> FlowyResult> { let workspace_id = self.user_service.workspace_id()?; - - let now = timestamp(); // This is safer than using SystemTime which could fail + let now = timestamp(); // First, try reading from the cache with expiration check let should_fetch = { @@ -298,16 +298,9 @@ impl AIManager { .await { Ok(list) => { - let models = list - .models - .into_iter() - .map(|m| m.name) - .collect::>(); - - // Update the cache with new timestamp - handle potential errors + let models = list.models; if let Err(err) = self.update_models_cache(&models, now).await { error!("Failed to update models cache: {}", err); - // Still return the fetched models even if caching failed } Ok(models) @@ -328,7 +321,11 @@ impl AIManager { } } - async fn update_models_cache(&self, models: &[String], timestamp: i64) -> FlowyResult<()> { + async fn update_models_cache( + &self, + models: &[AvailableModel], + timestamp: i64, + ) -> FlowyResult<()> { match self.server_models.try_write() { Ok(mut cache) => { cache.models = models.to_vec(); @@ -360,8 +357,8 @@ impl AIManager { .get_server_available_models() .await? .into_iter() - .map(|name| AIModelPB { - name, + .map(|m| AIModelPB { + name: m.name, is_local: false, }) .collect(); diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 355e8d27b5..ace7e3d807 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; +use flowy_ai_pub::cloud::ai_dto::AvailableModel; use flowy_ai_pub::cloud::{ AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, @@ -186,7 +187,29 @@ pub struct ChatMessageListPB { #[derive(Default, ProtoBuf, Clone, Debug)] pub struct ServerAvailableModelsPB { #[pb(index = 1)] - pub models: String, + pub models: Vec, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct AvailableModelPB { + #[pb(index = 1)] + pub name: String, + + #[pb(index = 2)] + pub is_default: bool, +} + +impl From for AvailableModelPB { + fn from(value: AvailableModel) -> Self { + let is_default = value + .metadata + .and_then(|v| v.get("is_default").map(|v| v.as_bool().unwrap_or(false))) + .unwrap_or(false); + Self { + name: value.name, + is_default, + } + } } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 1418e84058..2bc182b9b1 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -7,7 +7,6 @@ use crate::entities::*; use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use serde_json::json; use std::sync::{Arc, Weak}; use tracing::trace; use validator::Validate; @@ -108,7 +107,10 @@ pub(crate) async fn get_server_model_list_handler( ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; let models = ai_manager.get_server_available_models().await?; - let models = serde_json::to_string(&json!({"models": models}))?; + let models = models + .into_iter() + .map(AvailableModelPB::from) + .collect::>(); data_result_ok(ServerAvailableModelsPB { models }) } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index aa9d38a9cd..36d5232bd2 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -77,10 +77,7 @@ impl From for UserProfilePB { EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), }; - let mut ai_model = user_profile.ai_model; - if ai_model.is_empty() { - ai_model = "Default".to_string(); - } + let ai_model = user_profile.ai_model; Self { id: user_profile.uid, email: user_profile.email, From 4c39908748faed8763cad58b65a3b707e1420b24 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 24 Mar 2025 22:21:44 +0800 Subject: [PATCH 204/384] chore: separate model --- .../desktop_prompt_text_field.dart | 103 ++++++++++++++---- frontend/resources/translations/en.json | 1 + 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index ba2d035183..bf21a59ad0 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -761,29 +761,94 @@ class _PopoverSelectModel extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return ListView.builder( - shrinkWrap: true, - itemCount: state.availableModels?.models.length ?? 0, + if (state.availableModels == null || + state.availableModels!.models.isEmpty) { + return const SizedBox.shrink(); + } + + // Separate models into local and cloud models + final localModels = state.availableModels!.models + .where((model) => model.isLocal) + .toList(); + + final cloudModels = state.availableModels!.models + .where((model) => !model.isLocal) + .toList(); + + return Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), - itemBuilder: (context, index) { - return _ModelItem( - model: state.availableModels!.models[index], - onTap: () { - context.read().add( - SelectModelEvent.selectModel( - state.availableModels!.models[index], - ), - ); - onClose(); - }, - ); - }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Local AI Models Section + if (localModels.isNotEmpty) ...[ + _ModelSectionHeader( + title: LocaleKeys.chat_changeFormat_localModel.tr(), + ), + const SizedBox(height: 4), + ...localModels.map( + (model) => _ModelItem( + model: model, + onTap: () { + context.read().add( + SelectModelEvent.selectModel(model), + ); + onClose(); + }, + ), + ), + const SizedBox(height: 8), + ], + + // Cloud AI Models Section + if (cloudModels.isNotEmpty) ...[ + if (localModels.isNotEmpty) + _ModelSectionHeader( + title: LocaleKeys.chat_changeFormat_cloudModel.tr(), + ), + const VSpace(4), + ...cloudModels.map( + (model) => _ModelItem( + model: model, + onTap: () { + context.read().add( + SelectModelEvent.selectModel(model), + ); + onClose(); + }, + ), + ), + ], + ], + ), ); }, ); } } +class _ModelSectionHeader extends StatelessWidget { + const _ModelSectionHeader({ + required this.title, + }); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 4, bottom: 2), + child: FlowyText( + title, + fontSize: 12, + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w500, + ), + ); + } +} + class _ModelItem extends StatelessWidget { const _ModelItem({ required this.model, @@ -795,10 +860,8 @@ class _ModelItem extends StatelessWidget { @override Widget build(BuildContext context) { - var modelName = model.name; - if (model.isLocal) { - modelName += " (${LocaleKeys.chat_changeFormat_localModel.tr()})"; - } + final modelName = model.name; + return FlowyTextButton( modelName, fillColor: Colors.transparent, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 12ae84cc8e..62de8be000 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -248,6 +248,7 @@ "blankDescription": "Format response", "defaultDescription": "Auto mode", "localModel": "Local Model", + "cloudModel": "Cloud Model", "switchModel": "Switch model", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", From 72d660f1acb3e9a3bda480970866a24d914a19ec Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:07:46 +0800 Subject: [PATCH 205/384] fix: number cell update flashes with old content (#7605) --- .../database/application/cell/bloc/number_cell_bloc.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart index df159b817b..73b2d2977b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart @@ -47,15 +47,6 @@ class NumberCellBloc extends Bloc { if (state.content != text) { emit(state.copyWith(content: text)); await cellController.saveCellData(text); - - // If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string. - // So for every cell data that will be formatted in the backend. - // It needs to get the formatted data after saving. - add( - NumberCellEvent.didReceiveCellUpdate( - cellController.getCellData(), - ), - ); } }, ); From 0dc23639627613927236b5136de2b7d955b5d8ff Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 25 Mar 2025 13:05:18 +0800 Subject: [PATCH 206/384] chore: fix incorrect local ai state --- .../lib/ai/service/ai_input_control.dart | 58 +++++++++---------- .../lib/startup/tasks/device_info_task.dart | 3 + 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart index 942c27a3c9..8e4b42d1ce 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart @@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.da import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:protobuf/protobuf.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -21,12 +20,14 @@ class AIModelStateNotifier { final bool _isDesktop; final LocalAIStateListener? _localAIListener; final AIModelSwitchListener _aiModelSwitchListener; + LocalAIPB? _localAIState; AvailableModelsPB? _availableModels; - // callbacks + // Callbacks void Function(AiType, bool, String)? onChanged; void Function(AvailableModelsPB)? onAvailableModelsChanged; + String hintText() { final aiType = getCurrentAiType(); if (aiType.isLocal) { @@ -38,22 +39,16 @@ class AIModelStateNotifier { } AiType getCurrentAiType() { - // On non-desktop platforms, always return cloud type - if (!_isDesktop) { - return AiType.cloud; - } - - return _availableModels?.selectedModel.isLocal == true + // On non-desktop platforms, always return cloud type. + if (!_isDesktop) return AiType.cloud; + return (_availableModels?.selectedModel.isLocal ?? false) ? AiType.local : AiType.cloud; } bool isEditable() { - // On non-desktop platforms, always editable (cloud-only) - if (!_isDesktop) { - return true; - } - + // On non-desktop platforms, always editable. + if (!_isDesktop) return true; return getCurrentAiType().isLocal ? _localAIState?.state == RunningStatePB.Running : true; @@ -64,7 +59,11 @@ class AIModelStateNotifier { } Future init() async { - await _loadAvailableModels(); + // Load both available models and local state concurrently. + await Future.wait([ + _loadAvailableModels(), + _loadLocalAIState(), + ]); } Future _loadAvailableModels() async { @@ -76,8 +75,20 @@ class AIModelStateNotifier { onAvailableModelsChanged?.call(models); _notifyStateChanged(); }, - (err) { - Log.error("Failed to get available models: $err"); + (err) => Log.error("Failed to get available models: $err"), + ); + } + + Future _loadLocalAIState() async { + final result = await AIEventGetLocalAIState().send(); + result.fold( + (state) { + _localAIState = state; + _notifyStateChanged(); + }, + (error) { + Log.error("Failed to get local AI state: $error"); + _notifyStateChanged(); }, ); } @@ -89,12 +100,11 @@ class AIModelStateNotifier { this.onChanged = onChanged; this.onAvailableModelsChanged = onAvailableModelsChanged; - // Only start local AI listener on desktop platforms + // Only start local AI listener on desktop platforms. if (_isDesktop) { _localAIListener?.start( stateCallback: (state) { _localAIState = state; - if (state.state == RunningStatePB.Running || state.state == RunningStatePB.Stopped) { _loadAvailableModels(); @@ -111,18 +121,8 @@ class AIModelStateNotifier { _availableModels = updatedModels; onAvailableModelsChanged?.call(updatedModels); } - if (model.isLocal && _isDesktop) { - AIEventGetLocalAIState().send().fold( - (localAIState) { - _localAIState = localAIState; - _notifyStateChanged(); - }, - (error) { - Log.error("Failed to get local AI state: $error"); - _notifyStateChanged(); - }, - ); + _loadLocalAIState(); } else { _notifyStateChanged(); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart index 81464f59d7..2c90afbdda 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart @@ -29,6 +29,9 @@ class ApplicationInfo { // If the latest version is greater than the current version, it means there is an update available static bool get isUpdateAvailable { try { + if (latestVersion.isEmpty) { + return false; + } return Version.parse(latestVersion) > Version.parse(applicationVersion); } catch (e) { return false; From ed44c20281a1aceeefa2063d8a4ee3194e082c6a Mon Sep 17 00:00:00 2001 From: ixicut Date: Tue, 25 Mar 2025 11:19:11 +0200 Subject: [PATCH 207/384] fix: correct translation inconsistencies --- frontend/resources/translations/uk-UA.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 45cc93fd43..8227fc4c53 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -64,7 +64,7 @@ "settings": "Налаштування", "magicLinkSent": "Magic Link надіслано!", "invalidEmail": "Будь ласка, введіть дійсну адресу електронної пошти", - "alreadyHaveAnAccount": "Вже є аккаунт?", + "alreadyHaveAnAccount": "Вже є акаунт?", "logIn": "Авторизуватися", "generalError": "Щось пішло не так. Будь ласка спробуйте пізніше", "limitRateError": "З міркувань безпеки ви можете запитувати чарівне посилання лише кожні 60 секунд", @@ -362,7 +362,7 @@ "helpCenter": "Центр допомоги", "add": "Додати", "yes": "Так", - "no": "Немає", + "no": "Ні", "clear": "Очистити", "remove": "Видалити", "dontRemove": "Не видаляйте", @@ -810,7 +810,7 @@ "description": "Ви впевнені, що хочете видалити {plan}? Ви негайно втратите доступ до функцій і переваг {plan}." } }, - "currentPeriodBadge": "ПОТОМ", + "currentPeriodBadge": "ПОТОЧНИЙ", "changePeriod": "Період зміни", "planPeriod": "{} період", "monthlyInterval": "Щомісяця", From eb4b015de8d8fb80de747a559f489a59b5833bb7 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:37:15 +0800 Subject: [PATCH 208/384] chore: bump editor plugin ref (#7610) --- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index dcaefebed4..2acfec76bb 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -99,8 +99,8 @@ packages: dependency: "direct main" description: path: "packages/appflowy_editor_plugins" - ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb - resolved-ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb + ref: "4efcff7" + resolved-ref: "4efcff720ed01dd4d0f5f88a9f1ff6f79f423caa" url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" source: git version: "0.0.6" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 19e30fa4a2..344232897f 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -186,7 +186,7 @@ dependency_overrides: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git path: "packages/appflowy_editor_plugins" - ref: "ca8289099e40e0d6ad0605fbbe01fde3091538bb" + ref: "4efcff7" sheet: git: From 039c191d1f653667e5d05c7b61d7b40b1900210c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:41:35 +0800 Subject: [PATCH 209/384] fix: ai text insertion (#7615) * fix: dont scroll to ai writer node if path not found * chore: rename text robot clear method and add reset * fix: insert position is off if using ai writer multiple times * chore: reorganize code * fix: undo not working after accept --- .../ai_writer_block_operations.dart | 30 ++++- .../ai/operations/ai_writer_cubit.dart | 117 +++++------------- .../ai/widgets/ai_writer_scroll_wrapper.dart | 8 +- .../base/markdown_text_robot.dart | 8 +- 4 files changed, 65 insertions(+), 98 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart index d48c655e56..1b495a5b23 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart @@ -48,16 +48,16 @@ Future removeAiWriterNode( ); } -void formatSelection( +Future formatSelection( EditorState editorState, Selection selection, - Transaction transaction, ApplySuggestionFormatType formatType, -) { +) async { final nodes = editorState.getNodesInSelection(selection).toList(); if (nodes.isEmpty) { return; } + final transaction = editorState.transaction; if (nodes.length == 1) { final node = nodes.removeAt(0); @@ -103,25 +103,43 @@ void formatSelection( } transaction.compose(); + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + withUpdateSelection: false, + ); } -Position ensurePreviousNodeIsEmptyParagraph( +Future ensurePreviousNodeIsEmptyParagraph( EditorState editorState, Node aiWriterNode, - Transaction transaction, -) { +) async { final previous = aiWriterNode.previous; final needsEmptyParagraphNode = previous == null || previous.type != ParagraphBlockKeys.type || (previous.delta?.toPlainText().isNotEmpty ?? false); final Position position; + final transaction = editorState.transaction; + if (needsEmptyParagraphNode) { position = Position(path: aiWriterNode.path); transaction.insertNode(aiWriterNode.path, paragraphNode()); } else { position = Position(path: previous.path); } + transaction.afterSelection = Selection.collapsed(position); + + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); return position; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 07d5d84ae9..20247bf3a3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -54,6 +54,7 @@ class AiWriterCubit extends Cubit { if (withDiscard) { await _textRobot.discard(); } + _textRobot.clear(); _textRobot.reset(); onRemoveNode?.call(); records.clear(); @@ -65,21 +66,11 @@ class AiWriterCubit extends Cubit { if (selection == null) { return; } - final transaction = editorState.transaction; - formatSelection( + await formatSelection( editorState, selection, - transaction, ApplySuggestionFormatType.clear, ); - await editorState.apply( - transaction, - options: const ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, - ); } if (aiWriterNode != null) { await removeAiWriterNode(editorState, aiWriterNode!); @@ -122,7 +113,7 @@ class AiWriterCubit extends Cubit { } await _textRobot.discard(); - _textRobot.reset(); + _textRobot.clear(); switch (command) { case AiWriterCommand.continueWriting: @@ -222,11 +213,15 @@ class AiWriterCubit extends Cubit { if (action case SuggestionAction.accept) { await _textRobot.persist(); + await formatSelection( + editorState, + selection, + ApplySuggestionFormatType.clear, + ); final nodes = editorState.getNodesInSelection(selection); final transaction = editorState.transaction..deleteNodes(nodes); await editorState.apply( transaction, - options: const ApplyOptions(recordUndo: false), withUpdateSelection: false, ); await exit(withDiscard: false, withUnformat: false); @@ -235,6 +230,8 @@ class AiWriterCubit extends Cubit { if (action case SuggestionAction.keep) { await _textRobot.persist(); + await exit(withDiscard: false); + return; } if (action case SuggestionAction.insertBelow) { @@ -244,20 +241,9 @@ class AiWriterCubit extends Cubit { final command = (state as ReadyAiWriterState).command; final markdownText = (state as ReadyAiWriterState).markdownText; if (command == AiWriterCommand.explain && markdownText.isNotEmpty) { - final transaction = editorState.transaction; - final position = ensurePreviousNodeIsEmptyParagraph( + final position = await ensurePreviousNodeIsEmptyParagraph( editorState, aiWriterNode!, - transaction, - ); - transaction.afterSelection = null; - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, ); _textRobot.start(position: position); await _textRobot.persist(markdownText: markdownText); @@ -265,21 +251,13 @@ class AiWriterCubit extends Cubit { await _textRobot.persist(); } - final transaction = editorState.transaction; - formatSelection( + await formatSelection( editorState, selection, - transaction, ApplySuggestionFormatType.clear, ); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - withUpdateSelection: false, - ); + await exit(withDiscard: false); } - - await exit(withDiscard: false); } bool hasUnusedResponse() { @@ -308,22 +286,20 @@ class AiWriterCubit extends Cubit { if (command == AiWriterCommand.continueWriting) { return (true, ''); - } else { - if (selection.isCollapsed) { - return (true, ''); - } else { - final selectionText = - await editorState.getMarkdownInSelection(selection); + } + if (selection.isCollapsed) { + return (true, ''); + } - if (command == AiWriterCommand.userQuestion) { - records.add( - AiWriterRecord.user(content: selectionText, format: null), - ); - return (true, ''); - } else { - return (true, selectionText); - } - } + final selectionText = await editorState.getMarkdownInSelection(selection); + + if (command == AiWriterCommand.userQuestion) { + records.add( + AiWriterRecord.user(content: selectionText, format: null), + ); + return (true, ''); + } else { + return (true, selectionText); } } @@ -360,19 +336,9 @@ class AiWriterCubit extends Cubit { sourceIds: selectedSourcesNotifier.value, completionType: command.toCompletionType(), onStart: () async { - final transaction = editorState.transaction; - final position = ensurePreviousNodeIsEmptyParagraph( + final position = await ensurePreviousNodeIsEmptyParagraph( editorState, aiWriterNode!, - transaction, - ); - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, ); _textRobot.start(position: position); records.add( @@ -461,20 +427,9 @@ class AiWriterCubit extends Cubit { sourceIds: selectedSourcesNotifier.value, format: predefinedFormat, onStart: () async { - final transaction = editorState.transaction; - final position = ensurePreviousNodeIsEmptyParagraph( + final position = await ensurePreviousNodeIsEmptyParagraph( editorState, aiWriterNode!, - transaction, - ); - transaction.afterSelection = null; - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, ); _textRobot.start(position: position); records.add( @@ -555,26 +510,14 @@ class AiWriterCubit extends Cubit { history: records, sourceIds: selectedSourcesNotifier.value, onStart: () async { - final transaction = editorState.transaction; - formatSelection( + await formatSelection( editorState, selection, - transaction, ApplySuggestionFormatType.original, ); - final position = ensurePreviousNodeIsEmptyParagraph( + final position = await ensurePreviousNodeIsEmptyParagraph( editorState, aiWriterNode!, - transaction, - ); - transaction.afterSelection = null; - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, ); _textRobot.start(position: position); records.add( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 8a054de1c0..326d1a44ab 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -198,9 +198,11 @@ class _AiWriterScrollWrapperState extends State { throttler.call(() { if (aiWriterCubit.aiWriterNode != null) { final path = aiWriterCubit.aiWriterNode!.path; - widget.editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); + if (path.isNotEmpty) { + widget.editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: path)), + ); + } } }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 62c024b31a..378c4d88cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -58,7 +58,7 @@ class MarkdownTextRobot { void start({ Position? position, }) { - _insertPosition ??= position ?? editorState.selection?.start; + _insertPosition = position ?? editorState.selection?.start; if (_enableDebug) { Log.info( @@ -148,11 +148,15 @@ class MarkdownTextRobot { } } - void reset() { + void clear() { _markdownText = ''; _insertedNodes = []; } + void reset() { + _insertPosition = null; + } + Future _refresh({ required bool inMemoryUpdate, bool updateSelection = false, From 5a0478ad562728e30984bb169f466c31cbe7e359 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 23:00:28 +0800 Subject: [PATCH 210/384] fix(mobile): settings page crashes (#7616) --- .../settings/ai/settings_ai_bloc.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 6dcd52b294..44558b13b7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -7,6 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; @@ -96,14 +97,17 @@ class SettingsAIBloc extends Bloc { }, didLoadAvailableModels: (List models) { if (state.selectedAIModel.isEmpty) { - final m = models.firstWhere((model) => model.isDefault); - _updateUserWorkspaceSetting(model: m.name); - emit( - state.copyWith( - availableModels: models, - selectedAIModel: m.name, - ), - ); + final defaultModel = + models.firstWhereOrNull((model) => model.isDefault); + if (defaultModel != null) { + _updateUserWorkspaceSetting(model: defaultModel.name); + emit( + state.copyWith( + availableModels: models, + selectedAIModel: defaultModel.name, + ), + ); + } } else { emit( state.copyWith( From 1d437fb81ef3e9a335a802fe8a627c96ba39b6e5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 23:44:00 +0800 Subject: [PATCH 211/384] chore: adjust ai writer popover content area (#7618) --- .../ai/ai_writer_block_component.dart | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index a35d0cfbf8..3e1316a58f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -423,28 +423,25 @@ class SecondaryContentArea extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + const VSpace(8.0), + Container( + height: 24.0, + padding: EdgeInsets.symmetric(horizontal: 14.0), + alignment: AlignmentDirectional.centerStart, + child: FlowyText( + command.i18n, + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF666D76), + ), + ), + const VSpace(4.0), Flexible( child: SingleChildScrollView( physics: ClampingScrollPhysics(), - padding: EdgeInsets.only(top: 8.0, left: 14.0, right: 14.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 24.0, - alignment: AlignmentDirectional.centerStart, - child: FlowyText( - command.i18n, - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF666D76), - ), - ), - const VSpace(4.0), - AIMarkdownText( - markdown: markdownText, - ), - ], + padding: EdgeInsets.symmetric(horizontal: 14.0), + child: AIMarkdownText( + markdown: markdownText, ), ), ), From c212c568e9f6876a168bdc97c61c910e8cbd3648 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 26 Mar 2025 11:00:41 +0800 Subject: [PATCH 212/384] chore: bump editor ref (#7621) --- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 2acfec76bb..d61c5338f5 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "50f9724" - resolved-ref: "50f9724190ee47eac3d7dbe323a3ce39d19ea883" + ref: f46e991 + resolved-ref: f46e991d0a9c5a95bd14be4cc96e68171c9ed9bc url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 344232897f..e005f7153d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "50f9724" + ref: "f46e991" appflowy_editor_plugins: git: From 1db6da7024ea247b7d15ac0be96d45ec91da8d99 Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 26 Mar 2025 13:22:47 +0800 Subject: [PATCH 213/384] fix: undo not working for toggle heading (#7598) --- .../editor_plugins/actions/block_action_option_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index 35e92d7170..abed98136d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -469,7 +469,7 @@ class BlockActionOptionCubit extends Cubit { blockComponentDelta: newDelta.toJson(), }, children: [ - ...node.children, + ...node.children.map((e) => e.deepCopy()), ...insertedNodes.map((e) => e.deepCopy()), ], ); From cfca70ae140aab41494bea7f90e70c9524a12c69 Mon Sep 17 00:00:00 2001 From: Ametero <101631790+Ameterius@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:23:25 -0700 Subject: [PATCH 214/384] =?UTF-8?q?chore:=20update=20translations=20with?= =?UTF-8?q?=20Fink=20=F0=9F=90=A6=20(#7584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/resources/translations/ar-SA.json | 4 +- frontend/resources/translations/ru-RU.json | 93 +++++++++++++++++++++- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 243cf0c177..62708e664f 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -2255,11 +2255,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل" + "clickToOpen": "انقر لفتح السجل", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index b89b26f0c5..6ca29ffc03 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -76,9 +76,14 @@ }, "workspace": { "chooseWorkspace": "Выберите рабочее пространство", + "defaultName": "Моё рабочее пространство", "create": "Создать рабочее пространство", + "new": "Новое рабочее пространство", + "importFromNotion": "Импортировать с Notion", + "learnMore": "Узнать больше", "reset": "Сбросить рабочее пространство", "renameWorkspace": "Переименовать рабочее пространство", + "workspaceNameCannotBeEmpty": "Название рабочего пространства не может быть пустым", "resetWorkspacePrompt": "Сброс рабочего пространства приведет к удалению всех страниц и данных внутри него. Вы уверены, что хотите сбросить рабочее пространство? В качестве альтернативы вы можете обратиться в службу поддержки для восстановления рабочего пространства", "hint": "рабочее пространство", "notFoundError": "Рабочее пространство не найдено.", @@ -122,7 +127,9 @@ "visitSite": "Посетить сайт", "exportAsTab": "Экспортировать как", "publishTab": "Опубликовать", - "shareTab": "Поделиться" + "shareTab": "Поделиться", + "publishOnAppFlowy": "Выложить на AppFlowy", + "shareTabTitle": "Пригласить к сотрудничеству" }, "moreAction": { "small": "маленький", @@ -144,6 +151,11 @@ "csv": "CSV", "database": "База данных" }, + "emojiIconPicker": { + "iconUploader": { + "change": "Изменить" + } + }, "disclosureAction": { "rename": "Переименовать", "delete": "Удалить", @@ -155,7 +167,8 @@ "addToFavorites": "Добавить в избранное", "copyLink": "Скопировать ссылку", "changeIcon": "Изменить иконку", - "collapseAllPages": "Свернуть все подстраницы" + "collapseAllPages": "Свернуть все подстраницы", + "lockPage": "Заблокировать страницу" }, "blankPageTitle": "Пустая страница", "newPageText": "Новая страница", @@ -170,17 +183,39 @@ "relatedQuestion": "Связано", "serverUnavailable": "Сервис временно недоступен. Пожалуйста, повторите попытку позже.", "aiServerUnavailable": "🌈 Ой-ой! 🌈. Единорог съел наш ответ. Пожалуйста, повторите попытку!", + "retry": "Повторить", "clickToRetry": "Нажмите, чтобы повторить попытку", "regenerateAnswer": "Повторно сгенерировать", "question1": "Как использовать канбан для управления задачами.", "question2": "Объясните метод GTD.", "question3": "Зачем использовать Rust.", "question4": "Рецепт из того, что есть у меня на кухне.", - "aiMistakePrompt": "ИИ может ошибаться. Проверяйте важную информацию." + "aiMistakePrompt": "ИИ может ошибаться. Проверяйте важную информацию.", + "inputActionNoPages": "Нет результатов на странице", + "currentPage": "Текущая страница", + "regenerate": "Попробуйте ещё раз", + "addToNewPage": "Создать новую страницу", + "openPagePreviewFailedToast": "Не удалось открыть страницу", + "changeFormat": { + "actionButton": "Изменить формат", + "textOnly": "Текст", + "imageOnly": "Только изображение", + "textAndImage": "Текст и изображение", + "text": "Параграф", + "bullet": "Список маркеров", + "number": "Нумерованный список", + "defaultDescription": "Автоматический режим" + }, + "selectBanner": { + "selectMessages": "Выбрать сообщения", + "allSelected": "Все выбрано" + }, + "stopTooltip": "Остановить генерацию" }, "trash": { "text": "Корзина", "restoreAll": "Восстановить всё", + "restore": "Восстановить", "deleteAll": "Удалить всё", "pageHeader": { "fileName": "Имя файла", @@ -195,6 +230,9 @@ "title": "Вы уверены, что хотите восстановить все страницы в корзине?", "caption": "Это действие не может быть отменено." }, + "restorePage": { + "caption": "Вы уверены, что хотите восстановить эту страницу?" + }, "mobile": { "actions": "Действия с корзиной", "empty": "Корзина пуста", @@ -289,7 +327,10 @@ "removeSuccess": "Удалено успешно", "favoriteSpace": "Избранное", "RecentSpace": "Недавнее", - "Spaces": "Пространства" + "Spaces": "Пространства", + "upgradeToPro": "Обновление до Pro", + "upgradeToAIMax": "Разблокируйте неограниченный ИИ", + "purchaseAIResponse": "Покупка " }, "notifications": { "export": { @@ -323,6 +364,7 @@ "upload": "Загрузить", "edit": "Редактировать", "delete": "Удалить", + "copy": "Копировать", "duplicate": "Дублировать", "putback": "Вернуть", "update": "Обновить", @@ -334,6 +376,7 @@ "helpCenter": "Центр помощи", "add": "Добавить", "yes": "Да", + "no": "Нет", "clear": "Очистить", "remove": "Удалить", "dontRemove": "Не удалять", @@ -349,6 +392,16 @@ "more": "Больше", "create": "Создать", "close": "Закрыть", + "next": "Следующий", + "previous": "Предыдущий", + "submit": "Представить", + "download": "Скачать", + "backToHome": "Вернуться на главную", + "viewing": "Просмотр", + "editing": "Редактирование", + "gotIt": "Понятно", + "retry": "Повторить попытку", + "uploadFailed": "Загрузка не удалась.", "tryAGain": "Попробовать ещё раз", "Done": "Готово", "Cancel": "Отмена", @@ -376,6 +429,28 @@ }, "settings": { "title": "Настройки", + "popupMenuItem": { + "settings": "Настройки", + "members": "Участники", + "helpAndSupport": "Помощь и поддержка" + }, + "sites": { + "namespaceTitle": "Пространство имен", + "namespaceDescription": "Управляйте своим пространством имен и домашней страницей", + "namespaceHeader": "Пространство имен", + "homepageHeader": "Домашняя страница", + "updateNamespace": "Обновить пространство имен", + "removeHomepage": "Удалить домашнюю страницу", + "selectHomePage": "Выберите страницу", + "clearHomePage": "Очистить домашнюю страницу для этого пространства имен", + "customUrl": "Пользовательский URL-адрес", + "namespace": { + "description": "Это изменение будет применено ко всем опубликованным страницам, размещенным в этом пространстве имен." + }, + "publishedPage": { + "page": "Страница" + } + }, "accountPage": { "menuLabel": "Мой аккаунт", "title": "Мой аккаунт", @@ -2183,5 +2258,15 @@ "privacyPolicy": "Политика конфиденциальности", "signInError": "Ошибка входа", "login": "Зарегистрироваться или войти" + }, + "ai": { + "limitReachedAction": { + "upgrade": "улучшить", + "proPlan": "план Pro", + "aiAddon": "Дополнение ИИ" + }, + "editing": "Редактирование", + "analyzing": "Анализ", + "more": "Более" } } From 24bb1b58a057c2876cd7f827b49afdf727c00615 Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 26 Mar 2025 13:24:16 +0800 Subject: [PATCH 215/384] feat: support use ":" keyword to create emojis (#7582) * feat: add ability to use : keyword to create emojis(#2797) * fix: emoji position error * chore: add integration test * chore: dismiss emoji picker while starting searching with space --- .../uncategorized/emoji_shortcut_test.dart | 112 +++++ .../uncategorized_test_runner_1.dart | 1 - .../shortcuts/character_shortcuts.dart | 5 + .../plugins/emoji/emoji_actions_command.dart | 44 ++ .../lib/plugins/emoji/emoji_handler.dart | 382 ++++++++++++++++++ .../lib/plugins/emoji/emoji_menu.dart | 204 ++++++++++ 6 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index 554a6eecbf..f539835b9b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:appflowy/plugins/emoji/emoji_handler.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,4 +41,114 @@ void main() { expect(find.byType(EmojiSelectionMenu), findsOneWidget); }); }); + + group('insert emoji by colon', () { + Future createNewDocumentAndShowEmojiList(WidgetTester tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.createNewPageWithNameUnderParent(); + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(':'); + await tester.pumpAndSettle(Duration(seconds: 1)); + } + + testWidgets('insert with click', (tester) async { + await createNewDocumentAndShowEmojiList(tester); + + /// emoji list is showing + final emojiHandler = find.byType(EmojiHandler); + expect(emojiHandler, findsOneWidget); + final emojiButtons = + find.descendant(of: emojiHandler, matching: find.byType(FlowyButton)); + final firstTextFinder = find.descendant( + of: emojiButtons.first, + matching: find.byType(FlowyText), + ); + final emojiText = + (firstTextFinder.evaluate().first.widget as FlowyText).text; + + /// click first emoji item + await tester.tapButton(emojiButtons.first); + final firstNode = + tester.editor.getCurrentEditorState().getNodeAtPath([0])!; + + /// except the emoji is in document + expect(emojiText.contains(firstNode.delta!.toPlainText()), true); + }); + + testWidgets('insert with arrow and enter', (tester) async { + await createNewDocumentAndShowEmojiList(tester); + + /// emoji list is showing + final emojiHandler = find.byType(EmojiHandler); + expect(emojiHandler, findsOneWidget); + final emojiButtons = + find.descendant(of: emojiHandler, matching: find.byType(FlowyButton)); + + /// tap arrow down and arrow up + await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown); + + final firstTextFinder = find.descendant( + of: emojiButtons.first, + matching: find.byType(FlowyText), + ); + final emojiText = + (firstTextFinder.evaluate().first.widget as FlowyText).text; + + /// tap enter + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + final firstNode = + tester.editor.getCurrentEditorState().getNodeAtPath([0])!; + + /// except the emoji is in document + expect(emojiText.contains(firstNode.delta!.toPlainText()), true); + }); + + testWidgets('insert with searching', (tester) async { + await createNewDocumentAndShowEmojiList(tester); + + /// search for `smiling eyes`, IME is not working, use keyboard input + final searchText = [ + LogicalKeyboardKey.keyS, + LogicalKeyboardKey.keyM, + LogicalKeyboardKey.keyI, + LogicalKeyboardKey.keyL, + LogicalKeyboardKey.keyI, + LogicalKeyboardKey.keyN, + LogicalKeyboardKey.keyG, + LogicalKeyboardKey.space, + LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyY, + LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyS, + ]; + + for (final key in searchText) { + await tester.simulateKeyEvent(key); + } + + /// tap enter + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + final firstNode = + tester.editor.getCurrentEditorState().getNodeAtPath([0])!; + + /// except the emoji is in document + expect(firstNode.delta!.toPlainText().contains('😄'), true); + }); + + testWidgets('start searching with sapce', (tester) async { + await createNewDocumentAndShowEmojiList(tester); + + /// emoji list is showing + final emojiHandler = find.byType(EmojiHandler); + expect(emojiHandler, findsOneWidget); + + /// input space + await tester.simulateKeyEvent(LogicalKeyboardKey.space); + + /// emoji list is dismissed + expect(emojiHandler, findsNothing); + }); + }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart index f7d94e8b4a..836cfe4ccd 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart @@ -13,7 +13,6 @@ void main() { hotkeys_test.main(); emoji_shortcut_test.main(); hotkeys_test.main(); - emoji_shortcut_test.main(); share_markdown_test.main(); import_files_test.main(); zoom_in_out_test.main(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart index 49178ca12b..13b2fea5ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -82,5 +83,9 @@ List buildCharacterShortcutEvents( documentBloc.documentId, styleCustomizer.inlineActionsMenuStyleBuilder(), ), + + /// show emoji list + /// - Using `:` + emojiCommand(context), ]; } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart new file mode 100644 index 0000000000..1942a1fd98 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart @@ -0,0 +1,44 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:universal_platform/universal_platform.dart'; + +import 'emoji_menu.dart'; + +const emojiCharacter = ':'; + +CharacterShortcutEvent emojiCommand(BuildContext context) => + CharacterShortcutEvent( + key: 'Opens Emoji Menu', + character: emojiCharacter, + handler: (editorState) { + emojiMenuService ??= EmojiMenu( + context: context, + editorState: editorState, + ); + return emojiCommandHandler(editorState, context); + }, + ); + +EmojiMenuService? emojiMenuService; + +Future emojiCommandHandler( + EditorState editorState, + BuildContext context, +) async { + final selection = editorState.selection; + + if (UniversalPlatform.isMobile || selection == null) { + return false; + } + + if (!selection.isCollapsed) { + await editorState.deleteSelection(selection); + } + + await editorState.insertTextAtPosition( + emojiCharacter, + position: selection.start, + ); + emojiMenuService?.show(); + return true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart new file mode 100644 index 0000000000..ec86de7e8d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart @@ -0,0 +1,382 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import 'emoji_menu.dart'; + +class EmojiHandler extends StatefulWidget { + const EmojiHandler({ + super.key, + required this.editorState, + required this.menuService, + required this.onDismiss, + required this.onSelectionUpdate, + required this.onEmojiSelect, + this.startCharAmount = 1, + this.cancelBySpaceHandler, + }); + + final EditorState editorState; + final EmojiMenuService menuService; + final VoidCallback onDismiss; + final VoidCallback onSelectionUpdate; + final SelectEmojiItemHandler onEmojiSelect; + final int startCharAmount; + final bool Function()? cancelBySpaceHandler; + + @override + State createState() => _EmojiHandlerState(); +} + +class _EmojiHandlerState extends State { + final _focusNode = FocusNode(debugLabel: 'emoji_menu_handler'); + final ItemScrollController controller = ItemScrollController(); + late EmojiData emojiData; + final List searchedEmojis = []; + bool loaded = false; + int invalidCounter = 0; + late int startOffset; + String _search = ''; + + set search(String search) { + _search = search; + _doSearch(); + } + + final ValueNotifier _selectedIndexNotifier = ValueNotifier(0); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _focusNode.requestFocus(), + ); + + startOffset = widget.editorState.selection?.endIndex ?? 0; + + if (kCachedEmojiData != null) { + loadEmojis(kCachedEmojiData!); + } else { + EmojiData.builtIn().then( + (value) { + kCachedEmojiData = value; + loadEmojis(value); + }, + ); + } + } + + @override + void dispose() { + _focusNode.dispose(); + _selectedIndexNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final noEmojis = searchedEmojis.isEmpty; + return Focus( + focusNode: _focusNode, + onKeyEvent: onKeyEvent, + child: Container( + constraints: const BoxConstraints(maxHeight: 400, maxWidth: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withAlpha(25), + ), + ], + ), + child: noEmojis + ? SizedBox( + width: 400, + height: 40, + child: Center( + child: SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(), + ), + ), + ) + : ScrollablePositionedList.builder( + itemCount: searchedEmojis.length, + itemScrollController: controller, + padding: EdgeInsets.all(8), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (ctx, index) { + return ValueListenableBuilder( + valueListenable: _selectedIndexNotifier, + builder: (context, value, __) { + final selectedEmoji = searchedEmojis[index]; + final displayedEmoji = + emojiData.getEmojiById(selectedEmoji.id); + final isSelected = value == index; + return SizedBox( + height: 32, + child: FlowyButton( + text: FlowyText.medium( + '$displayedEmoji ${selectedEmoji.name}', + lineHeight: 1.0, + overflow: TextOverflow.ellipsis, + ), + isSelected: isSelected, + onTap: () => onSelect(index), + ), + ); + }, + ); + }, + ), + ), + ); + } + + void changeSelectedIndex(int index) => _selectedIndexNotifier.value = index; + + void loadEmojis(EmojiData data) { + emojiData = data; + searchedEmojis.clear(); + searchedEmojis.addAll(emojiData.emojis.values); + if (mounted) { + setState(() { + loaded = true; + }); + } + } + + Future _doSearch() async { + if (!loaded) return; + if (_search.startsWith(' ')) { + widget.onDismiss.call(); + return; + } + final searchEmojiData = emojiData.filterByKeyword(_search); + setState(() { + searchedEmojis.clear(); + searchedEmojis.addAll(searchEmojiData.emojis.values); + changeSelectedIndex(0); + }); + if (searchedEmojis.isEmpty) { + widget.onDismiss.call(); + } + } + + KeyEventResult onKeyEvent(focus, KeyEvent event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + + const moveKeys = [ + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown, + ]; + + if (event.logicalKey == LogicalKeyboardKey.enter) { + onSelect(_selectedIndexNotifier.value); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.escape) { + // Workaround to bring focus back to editor + widget.editorState + .updateSelectionWithReason(widget.editorState.selection); + widget.onDismiss.call(); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + if (_search.isEmpty) { + if (_canDeleteLastCharacter()) { + widget.editorState.deleteBackward(); + } else { + // Workaround for editor regaining focus + widget.editorState.apply( + widget.editorState.transaction + ..afterSelection = widget.editorState.selection, + ); + } + widget.onDismiss.call(); + } else { + widget.onSelectionUpdate(); + widget.editorState.deleteBackward(); + _deleteCharacterAtSelection(); + } + + return KeyEventResult.handled; + } else if (event.character != null && + ![ + ...moveKeys, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, + ].contains(event.logicalKey)) { + /// Prevents dismissal of context menu by notifying the parent + /// that the selection change occurred from the handler. + widget.onSelectionUpdate(); + + if (event.logicalKey == LogicalKeyboardKey.space) { + final cancelBySpaceHandler = widget.cancelBySpaceHandler; + if (cancelBySpaceHandler != null && cancelBySpaceHandler()) { + return KeyEventResult.handled; + } + } + + // Interpolation to avoid having a getter for private variable + _insertCharacter(event.character!); + return KeyEventResult.handled; + } else if (moveKeys.contains(event.logicalKey)) { + _moveSelection(event.logicalKey); + return KeyEventResult.handled; + } + + if ([LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight] + .contains(event.logicalKey)) { + widget.onSelectionUpdate(); + + event.logicalKey == LogicalKeyboardKey.arrowLeft + ? widget.editorState.moveCursorForward() + : widget.editorState.moveCursorBackward(SelectionMoveRange.character); + + /// If cursor moves before @ then dismiss menu + /// If cursor moves after @search.length then dismiss menu + final selection = widget.editorState.selection; + if (selection != null && + (selection.endIndex < startOffset || + selection.endIndex > (startOffset + _search.length))) { + widget.onDismiss.call(); + } + + /// Workaround: When using the move cursor methods, it seems the + /// focus goes back to the editor, this makes sure this handler + /// receives the next keypress. + /// + _focusNode.requestFocus(); + + return KeyEventResult.handled; + } + + return KeyEventResult.handled; + } + + void onSelect(int index) { + widget.onEmojiSelect.call( + context, + ( + startOffset - widget.startCharAmount, + _search.length + widget.startCharAmount + ), + emojiData.getEmojiById(searchedEmojis[index].id), + ); + widget.onDismiss.call(); + } + + void _insertCharacter(String character) { + widget.editorState.insertTextAtCurrentSelection(character); + + final selection = widget.editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + + final delta = widget.editorState.getNodeAtPath(selection.end.path)?.delta; + if (delta == null) { + return; + } + + search = widget.editorState + .getTextInSelection( + selection.copyWith( + start: selection.start.copyWith(offset: startOffset), + end: selection.start + .copyWith(offset: startOffset + _search.length + 1), + ), + ) + .join(); + } + + void _moveSelection(LogicalKeyboardKey key) { + bool didChange = false; + final index = _selectedIndexNotifier.value; + if (key == LogicalKeyboardKey.arrowUp || + (key == LogicalKeyboardKey.tab && + HardwareKeyboard.instance.isShiftPressed)) { + if (index == 0) { + changeSelectedIndex(max(0, searchedEmojis.length - 1)); + didChange = true; + } else if (index > 0) { + changeSelectedIndex(index - 1); + didChange = true; + } + } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab] + .contains(key)) { + if (index < searchedEmojis.length - 1) { + changeSelectedIndex(index + 1); + didChange = true; + } else if (index == searchedEmojis.length - 1) { + changeSelectedIndex(0); + didChange = true; + } + } + + if (mounted && didChange) { + _scrollToItem(); + } + } + + void _scrollToItem() { + final noEmojis = searchedEmojis.isEmpty; + if (noEmojis) return; + controller.scrollTo( + index: _selectedIndexNotifier.value, + duration: const Duration(milliseconds: 200), + alignment: 0.5, + ); + } + + void _deleteCharacterAtSelection() { + final selection = widget.editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + + final node = widget.editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + + search = delta.toPlainText().substring( + startOffset, + startOffset - 1 + _search.length, + ); + } + + bool _canDeleteLastCharacter() { + final selection = widget.editorState.selection; + if (selection == null || !selection.isCollapsed) { + return false; + } + + final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta; + if (delta == null) { + return false; + } + + return delta.isNotEmpty; + } +} + +typedef SelectEmojiItemHandler = void Function( + BuildContext context, + (int start, int end) replacement, + String emoji, +); diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart new file mode 100644 index 0000000000..58269a32d5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart @@ -0,0 +1,204 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +import 'emoji_actions_command.dart'; +import 'emoji_handler.dart'; + +abstract class EmojiMenuService { + void show(); + + void dismiss(); +} + +class EmojiMenu extends EmojiMenuService { + EmojiMenu({ + required this.context, + required this.editorState, + this.startCharAmount = 1, + this.cancelBySpaceHandler, + }); + + final BuildContext context; + final EditorState editorState; + final bool Function()? cancelBySpaceHandler; + + final int startCharAmount; + + OverlayEntry? _menuEntry; + bool selectionChangedByMenu = false; + + @override + void dismiss() { + if (_menuEntry != null) { + editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); + keepEditorFocusNotifier.decrease(); + } + + _menuEntry?.remove(); + _menuEntry = null; + + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + editorState.service.selectionServiceKey.currentState == null; + if (!isSelectionDisposed) { + final selectionService = editorState.service.selectionService; + selectionService.currentSelection.removeListener(_onSelectionChange); + } + emojiMenuService = null; + } + + void _onSelectionUpdate() => selectionChangedByMenu = true; + + @override + void show() { + WidgetsBinding.instance.addPostFrameCallback((_) => _show()); + } + + void _show() { + final selectionService = editorState.service.selectionService; + final selectionRects = selectionService.selectionRects; + if (selectionRects.isEmpty) { + return; + } + + const menuHeight = 400.0, menuWidth = 300.0; + const Offset menuOffset = Offset(0, 10); + final Offset editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final Size editorSize = editorState.renderBox!.size; + final editorHeight = editorSize.height, editorWidth = editorSize.width; + // Default to opening the overlay below + Alignment alignment = Alignment.topLeft; + + final firstRect = selectionRects.first; + Offset offset = firstRect.bottomRight + menuOffset; + + // Show above + if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) { + offset = firstRect.topRight - menuOffset; + alignment = Alignment.bottomLeft; + offset = Offset( + offset.dx, + editorHeight - offset.dy, + ); + } + + // Show on the left + if (offset.dx > (editorWidth - menuWidth)) { + alignment = alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + offset = Offset( + editorWidth - offset.dx, + offset.dy, + ); + } + + final (left, top, right, bottom) = _getPosition(alignment, offset); + + _menuEntry = OverlayEntry( + builder: (context) => SizedBox( + height: editorSize.height, + width: editorSize.width, + + // GestureDetector handles clicks outside of the context menu, + // to dismiss the context menu. + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: dismiss, + child: Stack( + children: [ + Positioned( + top: top, + bottom: bottom, + left: left, + right: right, + child: EmojiHandler( + editorState: editorState, + menuService: this, + onDismiss: dismiss, + onSelectionUpdate: _onSelectionUpdate, + startCharAmount: startCharAmount, + cancelBySpaceHandler: cancelBySpaceHandler, + onEmojiSelect: ( + BuildContext context, + (int, int) replacement, + String emoji, + ) async { + final selection = editorState.selection; + + if (selection == null) return; + final node = + editorState.document.nodeAtPath(selection.end.path); + if (node == null) return; + final transaction = editorState.transaction + ..replaceText( + node, + replacement.$1, + replacement.$2, + emoji, + ); + await editorState.apply(transaction); + }, + ), + ), + ], + ), + ), + ), + ); + + Overlay.of(context).insert(_menuEntry!); + + editorState.service.keyboardService?.disable(showCursor: true); + editorState.service.scrollService?.disable(); + selectionService.currentSelection.addListener(_onSelectionChange); + } + + void _onSelectionChange() { + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + editorState.service.selectionServiceKey.currentState == null; + if (!isSelectionDisposed) { + final selectionService = editorState.service.selectionService; + if (selectionService.currentSelection.value == null) { + return; + } + } + + if (!selectionChangedByMenu) { + return dismiss(); + } + + selectionChangedByMenu = false; + } + + (double? left, double? top, double? right, double? bottom) _getPosition( + Alignment alignment, + Offset offset, + ) { + double? left, top, right, bottom; + switch (alignment) { + case Alignment.topLeft: + left = offset.dx; + top = offset.dy; + break; + case Alignment.bottomLeft: + left = offset.dx; + bottom = offset.dy; + break; + case Alignment.topRight: + right = offset.dx; + top = offset.dy; + break; + case Alignment.bottomRight: + right = offset.dx; + bottom = offset.dy; + break; + } + + return (left, top, right, bottom); + } +} From 9115e208ac699c052c211d513891f2b23c096a6e Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 26 Mar 2025 13:31:32 +0800 Subject: [PATCH 216/384] fix: numbered list generated by ai should keep the same index as the input (#7622) Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com> --- .../ai/operations/ai_writer_cubit.dart | 2 +- .../base/markdown_text_robot.dart | 52 +++++++++++++++++-- .../numbered_list/numbered_list_icon.dart | 35 ++++++++++--- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 20247bf3a3..3275f0d75c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -519,7 +519,7 @@ class AiWriterCubit extends Cubit { editorState, aiWriterNode!, ); - _textRobot.start(position: position); + _textRobot.start(position: position, previousSelection: selection); records.add( AiWriterRecord.user( content: prompt, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 378c4d88cc..d6cfee929c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -1,8 +1,10 @@ import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:collection/collection.dart'; import 'package:synchronized/synchronized.dart'; const _enableDebug = false; @@ -28,6 +30,9 @@ class MarkdownTextRobot { /// Only for debug via [_enableDebug]. final List _debugMarkdownTexts = []; + /// Selection before the refresh. + Selection? _previousSelection; + bool get hasAnyResult => _markdownText.isNotEmpty; String get markdownText => _markdownText; @@ -56,9 +61,11 @@ class MarkdownTextRobot { } void start({ + Selection? previousSelection, Position? position, }) { _insertPosition = position ?? editorState.selection?.start; + _previousSelection = previousSelection ?? editorState.selection; if (_enableDebug) { Log.info( @@ -175,11 +182,40 @@ class MarkdownTextRobot { tableWidth: 250.0, ).root.children; + // check if the first selected node before the refresh is a numbered list node + final previousSelection = _previousSelection; + final previousSelectedNode = previousSelection == null + ? null + : editorState.getNodeAtPath(previousSelection.start.path); + final firstNodeIsNumberedList = previousSelectedNode != null && + previousSelectedNode.type == NumberedListBlockKeys.type; + final newNodes = attributes == null ? documentNodes - : documentNodes - .map((node) => _styleDelta(node: node, attributes: attributes)) - .toList(); + : documentNodes.mapIndexed((index, node) { + final n = _styleDelta(node: node, attributes: attributes); + n.externalValues = AINodeExternalValues( + isAINode: true, + ); + if (index == 0 && n.type == NumberedListBlockKeys.type) { + if (firstNodeIsNumberedList) { + final builder = NumberedListIndexBuilder( + editorState: editorState, + node: previousSelectedNode, + ); + final firstIndex = builder.indexInSameLevel; + n.updateAttributes({ + NumberedListBlockKeys.number: firstIndex, + }); + } + + n.externalValues = AINodeExternalValues( + isAINode: true, + isFirstNumberedListNode: true, + ); + } + return n; + }).toList(); if (newNodes.isEmpty) { return; @@ -247,3 +283,13 @@ class MarkdownTextRobot { ); } } + +class AINodeExternalValues extends NodeExternalValues { + const AINodeExternalValues({ + this.isAINode = false, + this.isFirstNumberedListNode = false, + }); + + final bool isAINode; + final bool isFirstNumberedListNode; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart index 3fad80bf32..f77083d21d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -31,7 +32,7 @@ class NumberedListIcon extends StatelessWidget { return Padding( padding: const EdgeInsets.only(left: 6.0, right: 10.0), child: Text( - node.levelString, + node.buildLevelString(context), style: adjustedTextStyle, strutStyle: StrutStyle.fromTextStyle(combinedTextStyle), textHeightBehavior: TextHeightBehavior( @@ -47,9 +48,12 @@ class NumberedListIcon extends StatelessWidget { } } -extension on Node { - String get levelString { - final builder = _NumberedListIconBuilder(node: this); +extension NumberedListNodeIndex on Node { + String buildLevelString(BuildContext context) { + final builder = NumberedListIndexBuilder( + editorState: context.read(), + node: this, + ); final indexInRootLevel = builder.indexInRootLevel; final indexInSameLevel = builder.indexInSameLevel; final level = indexInRootLevel % 3; @@ -62,11 +66,13 @@ extension on Node { } } -class _NumberedListIconBuilder { - _NumberedListIconBuilder({ +class NumberedListIndexBuilder { + NumberedListIndexBuilder({ + required this.editorState, required this.node, }); + final EditorState editorState; final Node node; // the level of the current node @@ -88,7 +94,13 @@ class _NumberedListIconBuilder { Node? previous = node.previous; // if the previous one is not a numbered list, then it is the first one - if (previous == null || previous.type != NumberedListBlockKeys.type) { + final aiNodeExternalValues = + node.externalValues?.unwrapOrNull(); + + if (previous == null || + previous.type != NumberedListBlockKeys.type || + (aiNodeExternalValues != null && + aiNodeExternalValues.isFirstNumberedListNode)) { return node.attributes[NumberedListBlockKeys.number] ?? level; } @@ -97,10 +109,17 @@ class _NumberedListIconBuilder { startNumber = previous.attributes[NumberedListBlockKeys.number] as int?; level++; previous = previous.previous; + + // break the loop if the start number is found when the current node is an AI node + if (aiNodeExternalValues != null && startNumber != null) { + return startNumber + level - 1; + } } + if (startNumber != null) { - return startNumber + level - 1; + level = startNumber + level - 1; } + return level; } } From 7372f5583cfc07ba537811b66dd7796b6366762f Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 26 Mar 2025 13:56:24 +0800 Subject: [PATCH 217/384] chore: bump version 0.8.8 (#7627) --- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 44337645fe..0b23c5df26 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.7" +APPFLOWY_VERSION = "0.8.8" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e005f7153d..4710d52b4d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.7 +version: 0.8.8 environment: flutter: ">=3.27.4" From 815bb11cde82e28bd2795e88ae8ace075a06561b Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 26 Mar 2025 14:19:57 +0800 Subject: [PATCH 218/384] chore: implement local ai config --- .../lib/ai/service/ai_input_control.dart | 2 +- .../home/mobile_home_setting_page.dart | 1 - .../setting/ai/ai_settings_group.dart | 24 ++-- .../application/ai_model_switch_listener.dart | 6 +- .../settings/ai/settings_ai_bloc.dart | 109 ++++----------- .../setting_ai_view/model_selection.dart | 15 ++- .../setting_ai_view/settings_ai_view.dart | 5 +- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-codegen/src/ts_event/mod.rs | 1 - frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 18 ++- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 125 ++++++++++-------- frontend/rust-lib/flowy-ai/src/entities.rs | 20 ++- .../rust-lib/flowy-ai/src/event_handler.rs | 22 +-- frontend/rust-lib/flowy-ai/src/event_map.rs | 2 +- .../flowy-ai/src/local_ai/controller.rs | 1 - frontend/rust-lib/flowy-ai/src/util.rs | 2 +- 17 files changed, 175 insertions(+), 186 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart index 8e4b42d1ce..c468fdd6e9 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart @@ -14,7 +14,7 @@ class AIModelStateNotifier { : _isDesktop = UniversalPlatform.isDesktop, _localAIListener = UniversalPlatform.isDesktop ? LocalAIStateListener() : null, - _aiModelSwitchListener = AIModelSwitchListener(chatId: objectId); + _aiModelSwitchListener = AIModelSwitchListener(objectId: objectId); final String objectId; final bool _isDesktop; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart index b5845f763e..09e22fc746 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart @@ -100,7 +100,6 @@ class _MobileHomeSettingPageState extends State { key: ValueKey(currentWorkspaceId), userProfile: userProfile, workspaceId: currentWorkspaceId, - currentWorkspaceMemberRole: state.currentWorkspace?.role, ), const SupportSettingGroup(), const AboutSettingGroup(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart index 3b75b08543..b43ada6e42 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart @@ -5,8 +5,6 @@ import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -18,12 +16,10 @@ class AiSettingsGroup extends StatelessWidget { super.key, required this.userProfile, required this.workspaceId, - this.currentWorkspaceMemberRole, }); final UserProfilePB userProfile; final String workspaceId; - final AFRolePB? currentWorkspaceMemberRole; @override Widget build(BuildContext context) { @@ -32,7 +28,6 @@ class AiSettingsGroup extends StatelessWidget { create: (context) => SettingsAIBloc( userProfile, workspaceId, - currentWorkspaceMemberRole, )..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { @@ -48,7 +43,7 @@ class AiSettingsGroup extends StatelessWidget { children: [ Flexible( child: FlowyText( - state.selectedAIModel, + state.availableModels?.selectedModel.name ?? "", color: theme.colorScheme.onSurface, overflow: TextOverflow.ellipsis, ), @@ -84,16 +79,19 @@ class AiSettingsGroup extends StatelessWidget { title: LocaleKeys.settings_aiPage_keys_llmModelType.tr(), builder: (_) { return Column( - children: availableModels - .mapIndexed( - (index, model) => FlowyOptionTile.checkbox( - text: model.name, - showTopBorder: index == 0, - isSelected: state.selectedAIModel == model.name, + children: (availableModels?.models ?? []) + .asMap() + .entries + .map( + (entry) => FlowyOptionTile.checkbox( + text: entry.value.name, + showTopBorder: entry.key == 0, + isSelected: + availableModels?.selectedModel.name == entry.value.name, onTap: () { context .read() - .add(SettingsAIEvent.selectModel(model.name)); + .add(SettingsAIEvent.selectModel(entry.value)); context.pop(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart index f25ea4deca..2cfc349bf8 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart @@ -12,14 +12,14 @@ import 'package:appflowy_result/appflowy_result.dart'; typedef OnUpdateSelectedModel = void Function(AIModelPB model); class AIModelSwitchListener { - AIModelSwitchListener({required this.chatId}) { - _parser = ChatNotificationParser(id: chatId, callback: _callback); + AIModelSwitchListener({required this.objectId}) { + _parser = ChatNotificationParser(id: objectId, callback: _callback); _subscription = RustStreamReceiver.listen( (observable) => _parser?.parse(observable), ); } - final String chatId; + final String objectId; StreamSubscription? _subscription; ChatNotificationParser? _parser; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 6dcd52b294..1d50cde480 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -1,5 +1,5 @@ +import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; import 'package:appflowy/user/application/user_listener.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-ai/entities.pb.dart'; @@ -10,48 +10,38 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; -part 'settings_ai_bloc.g.dart'; class SettingsAIBloc extends Bloc { SettingsAIBloc( this.userProfile, this.workspaceId, - AFRolePB? currentWorkspaceMemberRole, ) : _userListener = UserListener(userProfile: userProfile), - _userService = UserBackendService(userId: userProfile.id), + _aiModelSwitchListener = + AIModelSwitchListener(objectId: "ai_models_global_active_model"), super( SettingsAIState( - selectedAIModel: userProfile.aiModel, userProfile: userProfile, - currentWorkspaceMemberRole: currentWorkspaceMemberRole, ), ) { + _aiModelSwitchListener.start( + onUpdateSelectedModel: (model) { + if (!isClosed) { + _loadModelList(); + } + }, + ); _dispatch(); - - if (currentWorkspaceMemberRole == null) { - _userService.getWorkspaceMember().then((result) { - result.fold( - (member) { - if (!isClosed) { - add(SettingsAIEvent.refreshMember(member)); - } - }, - (err) { - Log.error(err); - }, - ); - }); - } } final UserListener _userListener; final UserProfilePB userProfile; - final UserBackendService _userService; final String workspaceId; + final AIModelSwitchListener _aiModelSwitchListener; @override Future close() async { await _userListener.stop(); + await _aiModelSwitchListener.stop(); return super.close(); } @@ -67,7 +57,6 @@ class SettingsAIBloc extends Bloc { } }, ); - _loadUserWorkspaceSetting(); _loadModelList(); }, didReceiveUserProfile: (userProfile) { @@ -82,38 +71,25 @@ class SettingsAIBloc extends Bloc { !(state.aiSettings?.disableSearchIndexing ?? false), ); }, - selectModel: (String model) async { - await _updateUserWorkspaceSetting(model: model); + selectModel: (AIModelPB model) async { + if (!model.isLocal) { + await _updateUserWorkspaceSetting(model: model.name); + } }, didLoadAISetting: (UseAISettingPB settings) { emit( state.copyWith( aiSettings: settings, - selectedAIModel: settings.aiModel, enableSearchIndexing: !settings.disableSearchIndexing, ), ); }, - didLoadAvailableModels: (List models) { - if (state.selectedAIModel.isEmpty) { - final m = models.firstWhere((model) => model.isDefault); - _updateUserWorkspaceSetting(model: m.name); - emit( - state.copyWith( - availableModels: models, - selectedAIModel: m.name, - ), - ); - } else { - emit( - state.copyWith( - availableModels: models, - ), - ); - } - }, - refreshMember: (member) { - emit(state.copyWith(currentWorkspaceMemberRole: member.role)); + didLoadAvailableModels: (AvailableModelsPB models) { + emit( + state.copyWith( + availableModels: models, + ), + ); }, ); }); @@ -148,24 +124,11 @@ class SettingsAIBloc extends Bloc { (err) => Log.error(err), ); - void _loadUserWorkspaceSetting() { - final payload = UserWorkspaceIdPB(workspaceId: workspaceId); - UserEventGetWorkspaceSetting(payload).send().then((result) { - result.fold((settings) { - if (!isClosed) { - add(SettingsAIEvent.didLoadAISetting(settings)); - } - }, (err) { - Log.error(err); - }); - }); - } - void _loadModelList() { AIEventGetServerAvailableModels().send().then((result) { - result.fold((config) { + result.fold((models) { if (!isClosed) { - add(SettingsAIEvent.didLoadAvailableModels(config.models)); + add(SettingsAIEvent.didLoadAvailableModels(models)); } }, (err) { Log.error(err); @@ -182,17 +145,15 @@ class SettingsAIEvent with _$SettingsAIEvent { ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; - const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = - _RefreshMember; - const factory SettingsAIEvent.selectModel(String model) = _SelectAIModel; + const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; const factory SettingsAIEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; const factory SettingsAIEvent.didLoadAvailableModels( - List models, + AvailableModelsPB models, ) = _DidLoadAvailableModels; } @@ -201,23 +162,7 @@ class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, UseAISettingPB? aiSettings, - @Default("Default") String selectedAIModel, - AFRolePB? currentWorkspaceMemberRole, - @Default([]) List availableModels, + AvailableModelsPB? availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } - -@JsonSerializable() -class ModelList { - ModelList({ - required this.models, - }); - - factory ModelList.fromJson(Map json) => - _$ModelListFromJson(json); - - final List models; - - Map toJson() => _$ModelListToJson(this); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 8cd2f31114..bec4013a36 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -15,6 +16,10 @@ class AIModelSelection extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + if (state.availableModels == null) { + return const SizedBox.shrink(); + } + return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( @@ -28,17 +33,17 @@ class AIModelSelection extends StatelessWidget { ), const Spacer(), Flexible( - child: SettingsDropdown( + child: SettingsDropdown( key: const Key('_AIModelSelection'), onChanged: (model) => context .read() .add(SettingsAIEvent.selectModel(model)), - selectedOption: state.selectedAIModel, - options: state.availableModels + selectedOption: state.availableModels!.selectedModel, + options: state.availableModels!.models .map( - (model) => buildDropdownMenuEntry( + (model) => buildDropdownMenuEntry( context, - value: model.name, + value: model, label: model.name, ), ) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 7102520ac1..0fc1ef293c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -42,9 +42,8 @@ class SettingsAIView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - SettingsAIBloc(userProfile, workspaceId, currentWorkspaceMemberRole) - ..add(const SettingsAIEvent.started()), + create: (_) => SettingsAIBloc(userProfile, workspaceId) + ..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { final children = [ diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6b6af73c8f..ba07ff9c3b 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cd991507870d225e76df41164deee356cd9921a7#cd991507870d225e76df41164deee356cd9921a7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=52ad76f21f8f3a7b510dae029836b5fe86479e5a#52ad76f21f8f3a7b510dae029836b5fe86479e5a" dependencies = [ "anyhow", "appflowy-plugin", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=cd991507870d225e76df41164deee356cd9921a7#cd991507870d225e76df41164deee356cd9921a7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=52ad76f21f8f3a7b510dae029836b5fe86479e5a#52ad76f21f8f3a7b510dae029836b5fe86479e5a" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index a660ebcb5e..843a939ae3 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,5 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cd991507870d225e76df41164deee356cd9921a7" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "cd991507870d225e76df41164deee356cd9921a7" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "52ad76f21f8f3a7b510dae029836b5fe86479e5a" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "52ad76f21f8f3a7b510dae029836b5fe86479e5a" } diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs index da8c05f360..97a7f5f529 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs @@ -153,7 +153,6 @@ pub fn parse_event_crate(event_crate: &TsEventCrate) -> Vec { attrs .iter() .filter(|attr| !attr.attrs.event_attrs.ignore) - .into_iter() .map(|variant| EventASTContext::from(&variant.attrs)) .collect::>() }, diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 4639a93501..29cef4afde 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -29,10 +29,26 @@ pub struct AIModel { pub is_local: bool, } +impl AIModel { + pub fn server(name: String) -> Self { + Self { + name, + is_local: false, + } + } + + pub fn local(name: String) -> Self { + Self { + name, + is_local: true, + } + } +} + impl Default for AIModel { fn default() -> Self { Self { - name: "default".to_string(), + name: "Auto".to_string(), is_local: false, } } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 2486637ef8..03a18dcdec 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -27,7 +27,7 @@ use lib_infra::util::timestamp; use std::path::PathBuf; use std::sync::{Arc, Weak}; use tokio::sync::RwLock; -use tracing::{error, info, trace}; +use tracing::{error, info, instrument, trace}; pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; @@ -62,6 +62,8 @@ struct ServerModelsCache { timestamp: Option, } +pub const GLOBAL_ACTIVE_MODEL_KEY: &str = "global_active_model"; + pub struct AIManager { pub cloud_service_wm: Arc, pub user_service: Arc, @@ -266,7 +268,7 @@ impl AIManager { Ok(()) } - pub async fn get_workspace_select_model(&self) -> FlowyResult { + async fn get_workspace_select_model(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let model = self .cloud_service_wm @@ -275,7 +277,7 @@ impl AIManager { Ok(model) } - pub async fn get_server_available_models(&self) -> FlowyResult> { + async fn get_server_available_models(&self) -> FlowyResult> { let workspace_id = self.user_service.workspace_id()?; let now = timestamp(); @@ -339,97 +341,108 @@ impl AIManager { } } - pub async fn update_selected_model(&self, source: String, model: AIModelPB) -> FlowyResult<()> { + pub async fn update_selected_model(&self, source: String, model: AIModel) -> FlowyResult<()> { let source_key = ai_available_models_key(&source); self .store_preferences - .set_object::(&source_key, &model.clone().into())?; + .set_object::(&source_key, &model)?; chat_notification_builder(&source, ChatNotification::DidUpdateSelectedModel) - .payload(model) + .payload(AIModelPB::from(model)) .send(); Ok(()) } + #[instrument(skip_all, level = "debug")] + pub async fn toggle_local_ai(&self) -> FlowyResult<()> { + let enabled = self.local_ai.toggle_local_ai().await?; + let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); + if enabled { + if let Some(name) = self.local_ai.get_plugin_chat_model() { + info!("Set global active model to local ai: {}", name); + let model = AIModel::local(name); + self.update_selected_model(source_key, model).await?; + } + } else { + info!("Set global active model to default"); + let global_active_model = self + .get_workspace_select_model() + .await + .map(AIModel::server) + .unwrap_or_else(|_| AIModel::default()); + + self + .update_selected_model(source_key, global_active_model) + .await?; + } + + Ok(()) + } + pub async fn get_available_models(&self, source: String) -> FlowyResult { // Build the models list from server models and mark them as non-local. let mut models: Vec = self .get_server_available_models() .await? .into_iter() - .map(|m| AIModelPB { - name: m.name, - is_local: false, - }) + .map(|m| AIModelPB::server(m.name)) .collect(); - // Optionally add the local plugin model. + // If user enable local ai, then add local ai model to the list. if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - models.push(AIModelPB { - name: local_model, - is_local: true, - }); + models.push(AIModelPB::local(local_model)); } if models.is_empty() { return Ok(AvailableModelsPB { models, - selected_model: None, + selected_model: AIModelPB::default(), }); } + // Global active model is the model selected by the user in the workspace settings. + let global_active_model = self + .get_workspace_select_model() + .await + .map(AIModel::server) + .unwrap_or_else(|_| AIModel::default()); + + let mut user_selected_model = global_active_model.clone(); let source_key = ai_available_models_key(&source); - // Retrieve stored selected model, if any. - let stored_selected = self.store_preferences.get_object::(&source_key); - - // Get workspace default model once. - let workspace_default = self.get_workspace_select_model().await.ok(); - - // Determine the effective selected model. - let effective_selected = stored_selected.unwrap_or_else(|| { - if let Some(ws_name) = workspace_default.clone() { - let model = AIModel { - name: ws_name, - is_local: false, - }; - // Store the default if not present. - let _ = self.store_preferences.set_object(&source_key, &model); - model - } else { - AIModel::default() - } - }); - - // Find a matching model in the available list. - let used_model = models - .iter() - .find(|m| m.name == effective_selected.name) - .cloned() - .or_else(|| { - // If no match, try to use the workspace default if available. - if let Some(ws_name) = workspace_default { - Some(AIModelPB { - name: ws_name, - is_local: false, - }) - } else { - models.first().cloned() + // If source is provided, try to get the user-selected model from the store. User selected + // model will be used as the active model if it exists. + match self.store_preferences.get_object::(&source_key) { + None => { + // when there is selected model and current local ai is active, then use local ai + if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { + user_selected_model = AIModel::from(local_ai_model.clone()); } - }); + }, + Some(model) => { + user_selected_model = model; + }, + } + + // If user selected model is not available in the list, use the global active model. + let active_model = models + .iter() + .find(|m| m.name == user_selected_model.name) + .cloned() + .or_else(|| Some(AIModelPB::from(global_active_model))); // Update the stored preference if a different model is used. - if let Some(ref used) = used_model { - if used.name != effective_selected.name { + if let Some(ref active_model) = active_model { + if active_model.name != user_selected_model.name { self .store_preferences - .set_object::(&source_key, &AIModel::from(used.clone()))?; + .set_object::(&source_key, &AIModel::from(active_model.clone()))?; } } Ok(AvailableModelsPB { models, - selected_model: used_model, + selected_model: active_model.unwrap_or_default(), }) } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index ace7e3d807..0e996228c6 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -234,8 +234,8 @@ pub struct AvailableModelsPB { #[pb(index = 1)] pub models: Vec, - #[pb(index = 2, one_of)] - pub selected_model: Option, + #[pb(index = 2)] + pub selected_model: AIModelPB, } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -247,6 +247,22 @@ pub struct AIModelPB { pub is_local: bool, } +impl AIModelPB { + pub fn server(name: String) -> Self { + Self { + name, + is_local: false, + } + } + + pub fn local(name: String) -> Self { + Self { + name, + is_local: true, + } + } +} + impl From for AIModelPB { fn from(model: AIModel) -> Self { Self { diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 2bc182b9b1..6bedebd261 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -1,10 +1,13 @@ use std::fs; use std::path::PathBuf; -use crate::ai_manager::AIManager; +use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; use crate::completion::AICompletion; use crate::entities::*; -use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader}; +use crate::util::ai_available_models_key; +use flowy_ai_pub::cloud::{ + AIModel, ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader, +}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use std::sync::{Arc, Weak}; @@ -104,14 +107,11 @@ pub(crate) async fn regenerate_response_handler( #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_server_model_list_handler( ai_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let models = ai_manager.get_server_available_models().await?; - let models = models - .into_iter() - .map(AvailableModelPB::from) - .collect::>(); - data_result_ok(ServerAvailableModelsPB { models }) + let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); + let models = ai_manager.get_available_models(source_key).await?; + data_result_ok(models) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -132,7 +132,7 @@ pub(crate) async fn update_selected_model_handler( let data = data.try_into_inner()?; let ai_manager = upgrade_ai_manager(ai_manager)?; ai_manager - .update_selected_model(data.source, data.selected_model) + .update_selected_model(data.source, AIModel::from(data.selected_model)) .await?; Ok(()) } @@ -286,7 +286,7 @@ pub(crate) async fn toggle_local_ai_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let _ = ai_manager.local_ai.toggle_local_ai().await?; + ai_manager.toggle_local_ai().await?; let state = ai_manager.local_ai.get_local_ai_state().await; data_result_ok(state) } diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 84c2266f0b..0e24ca6a21 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -115,7 +115,7 @@ pub enum AIEvent { #[event(input = "RegenerateResponsePB")] RegenerateResponse = 27, - #[event(output = "ServerAvailableModelsPB")] + #[event(output = "AvailableModelsPB")] GetServerAvailableModels = 28, #[event(output = "LocalAISettingPB")] diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 04d73831b9..503345e206 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -322,7 +322,6 @@ impl LocalAIController { let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); self.store_preferences.set_bool(&key, enabled)?; self.toggle_plugin(enabled).await?; - Ok(enabled) } diff --git a/frontend/rust-lib/flowy-ai/src/util.rs b/frontend/rust-lib/flowy-ai/src/util.rs index d1667ae98b..a181d1b1d3 100644 --- a/frontend/rust-lib/flowy-ai/src/util.rs +++ b/frontend/rust-lib/flowy-ai/src/util.rs @@ -1,3 +1,3 @@ pub fn ai_available_models_key(object_id: &str) -> String { - format!("ai_available_models_{}", object_id) + format!("ai_models_{}", object_id) } From b3b13e550da75aa8521a078a7b8ecccc0f2ea962 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:29:11 +0800 Subject: [PATCH 219/384] chore: adjust popover shadows (#7626) --- .../ai/ai_writer_block_component.dart | 16 +++----- .../ai_writer_prompt_input_more_button.dart | 11 ++--- .../src/flowy_overlay/appflowy_popover.dart | 40 +++++++++---------- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 3e1316a58f..a4bc4f50e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -226,7 +226,7 @@ class _OverlayContentState extends State { final showSuggestedActionsWithin = showSuggestedActions && markdownText.isNotEmpty; - final darkBorderColor = Theme.of(context).isLightMode + final borderColor = Theme.of(context).isLightMode ? Color(0x1F1F2329) : Color(0xFF505469); @@ -241,7 +241,7 @@ class _OverlayContentState extends State { context, color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderColor: darkBorderColor, + borderColor: borderColor, ), child: SuggestionActionBar( currentCommand: command, @@ -257,7 +257,7 @@ class _OverlayContentState extends State { decoration: _getModalDecoration( context, color: null, - borderColor: darkBorderColor, + borderColor: borderColor, borderRadius: BorderRadius.all(Radius.circular(12.0)), ), constraints: BoxConstraints(maxHeight: 400), @@ -341,13 +341,9 @@ class _OverlayContentState extends State { strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: borderRadius, - boxShadow: const [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 20, - color: Color(0x1A1F2329), - ), - ], + boxShadow: Theme.of(context).isLightMode + ? ShadowConstants.lightSmall + : ShadowConstants.darkSmall, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart index eefb122a20..1bd80d081d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart @@ -1,6 +1,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; @@ -102,13 +103,9 @@ class MoreAiWriterCommands extends StatelessWidget { strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: BorderRadius.all(Radius.circular(8.0)), - boxShadow: const [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 20, - color: Color(0x1A1F2329), - ), - ], + boxShadow: Theme.of(context).isLightMode + ? ShadowConstants.lightSmall + : ShadowConstants.darkSmall, ), child: IntrinsicWidth( child: Column( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 0ef500d967..2da547afda 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -4,6 +4,23 @@ import 'package:flutter/material.dart'; export 'package:appflowy_popover/appflowy_popover.dart'; +class ShadowConstants { + ShadowConstants._(); + + static const List lightSmall = [ + BoxShadow(offset: Offset(0, 4), blurRadius: 20, color: Color(0x1A1F2329)), + ]; + static const List lightMedium = [ + BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x121F2225)), + ]; + static const List darkSmall = [ + BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x5C000000)), + ]; + static const List darkMedium = [ + BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x5C000000)), + ]; +} + class AppFlowyPopover extends StatelessWidget { const AppFlowyPopover({ super.key, @@ -162,26 +179,9 @@ extension PopoverDecoration on BuildContext { final borderColor = Theme.of(this).brightness == Brightness.light ? ColorSchemeConstants.lightBorderColor : ColorSchemeConstants.darkBorderColor; - final shadows = [ - const BoxShadow( - color: Color(0x0A1F2329), - blurRadius: 24, - offset: Offset(0, 8), - spreadRadius: 8, - ), - const BoxShadow( - color: Color(0x0A1F2329), - blurRadius: 12, - offset: Offset(0, 6), - spreadRadius: 0, - ), - const BoxShadow( - color: Color(0x0F1F2329), - blurRadius: 8, - offset: Offset(0, 4), - spreadRadius: -8, - ) - ]; + final shadows = Theme.of(this).brightness == Brightness.light + ? ShadowConstants.lightSmall + : ShadowConstants.darkSmall; return ShapeDecoration( color: color ?? Theme.of(this).cardColor, shape: RoundedRectangleBorder( From eb0cff36c92a9fce87af2faef5555d4d381d6162 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:15:20 +0800 Subject: [PATCH 220/384] chore: improve ai model selection ui --- frontend/appflowy_flutter/lib/ai/ai.dart | 3 + .../lib/ai/service/ai_entities.dart | 8 + .../lib/ai/service/ai_input_control.dart | 138 ---------- .../ai/service/ai_model_state_notifier.dart | 174 +++++++++++++ .../lib/ai/service/ai_prompt_input_bloc.dart | 42 ++- .../lib/ai/service/select_model_bloc.dart | 98 ++++--- .../desktop_prompt_text_field.dart | 240 +----------------- .../predefined_format_buttons.dart | 2 + .../prompt_input/select_model_menu.dart | 221 ++++++++++++++++ .../ai_chat/application/chat_bloc.dart | 9 +- .../lib/plugins/ai_chat/chat_page.dart | 7 +- .../message/ai_change_model_bottom_sheet.dart | 145 +++++++++++ .../message/ai_message_action_bar.dart | 87 +++++++ .../message/ai_message_bubble.dart | 35 +++ .../presentation/message/ai_text_message.dart | 3 + frontend/resources/translations/en.json | 10 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 19 +- frontend/rust-lib/flowy-ai/src/entities.rs | 3 + .../rust-lib/flowy-ai/src/event_handler.rs | 1 + 19 files changed, 797 insertions(+), 448 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart create mode 100644 frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart create mode 100644 frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart diff --git a/frontend/appflowy_flutter/lib/ai/ai.dart b/frontend/appflowy_flutter/lib/ai/ai.dart index e3f52a8168..9bfeeb4e00 100644 --- a/frontend/appflowy_flutter/lib/ai/ai.dart +++ b/frontend/appflowy_flutter/lib/ai/ai.dart @@ -2,6 +2,8 @@ export 'service/ai_entities.dart'; export 'service/ai_prompt_input_bloc.dart'; export 'service/appflowy_ai_service.dart'; export 'service/error.dart'; +export 'service/ai_model_state_notifier.dart'; +export 'service/select_model_bloc.dart'; export 'widgets/loading_indicator.dart'; export 'widgets/prompt_input/action_buttons.dart'; export 'widgets/prompt_input/desktop_prompt_text_field.dart'; @@ -13,4 +15,5 @@ export 'widgets/prompt_input/mentioned_page_text_span.dart'; export 'widgets/prompt_input/predefined_format_buttons.dart'; export 'widgets/prompt_input/select_sources_bottom_sheet.dart'; export 'widgets/prompt_input/select_sources_menu.dart'; +export 'widgets/prompt_input/select_model_menu.dart'; export 'widgets/prompt_input/send_button.dart'; diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart index b8592bc32b..249a92019a 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart @@ -4,6 +4,14 @@ import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; +enum AiType { + cloud, + local; + + bool get isCloud => this == cloud; + bool get isLocal => this == local; +} + class PredefinedFormat extends Equatable { const PredefinedFormat({ required this.imageFormat, diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart b/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart deleted file mode 100644 index c468fdd6e9..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_input_control.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:protobuf/protobuf.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class AIModelStateNotifier { - AIModelStateNotifier({required this.objectId}) - : _isDesktop = UniversalPlatform.isDesktop, - _localAIListener = - UniversalPlatform.isDesktop ? LocalAIStateListener() : null, - _aiModelSwitchListener = AIModelSwitchListener(objectId: objectId); - - final String objectId; - final bool _isDesktop; - final LocalAIStateListener? _localAIListener; - final AIModelSwitchListener _aiModelSwitchListener; - - LocalAIPB? _localAIState; - AvailableModelsPB? _availableModels; - - // Callbacks - void Function(AiType, bool, String)? onChanged; - void Function(AvailableModelsPB)? onAvailableModelsChanged; - - String hintText() { - final aiType = getCurrentAiType(); - if (aiType.isLocal) { - return isEditable() - ? LocaleKeys.chat_inputLocalAIMessageHint.tr() - : LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); - } - return LocaleKeys.chat_inputMessageHint.tr(); - } - - AiType getCurrentAiType() { - // On non-desktop platforms, always return cloud type. - if (!_isDesktop) return AiType.cloud; - return (_availableModels?.selectedModel.isLocal ?? false) - ? AiType.local - : AiType.cloud; - } - - bool isEditable() { - // On non-desktop platforms, always editable. - if (!_isDesktop) return true; - return getCurrentAiType().isLocal - ? _localAIState?.state == RunningStatePB.Running - : true; - } - - void _notifyStateChanged() { - onChanged?.call(getCurrentAiType(), isEditable(), hintText()); - } - - Future init() async { - // Load both available models and local state concurrently. - await Future.wait([ - _loadAvailableModels(), - _loadLocalAIState(), - ]); - } - - Future _loadAvailableModels() async { - final payload = AvailableModelsQueryPB(source: objectId); - final result = await AIEventGetAvailableModels(payload).send(); - result.fold( - (models) { - _availableModels = models; - onAvailableModelsChanged?.call(models); - _notifyStateChanged(); - }, - (err) => Log.error("Failed to get available models: $err"), - ); - } - - Future _loadLocalAIState() async { - final result = await AIEventGetLocalAIState().send(); - result.fold( - (state) { - _localAIState = state; - _notifyStateChanged(); - }, - (error) { - Log.error("Failed to get local AI state: $error"); - _notifyStateChanged(); - }, - ); - } - - void startListening({ - void Function(AiType, bool, String)? onChanged, - void Function(AvailableModelsPB)? onAvailableModelsChanged, - }) { - this.onChanged = onChanged; - this.onAvailableModelsChanged = onAvailableModelsChanged; - - // Only start local AI listener on desktop platforms. - if (_isDesktop) { - _localAIListener?.start( - stateCallback: (state) { - _localAIState = state; - if (state.state == RunningStatePB.Running || - state.state == RunningStatePB.Stopped) { - _loadAvailableModels(); - } - }, - ); - } - - _aiModelSwitchListener.start( - onUpdateSelectedModel: (model) { - if (_availableModels != null) { - final updatedModels = _availableModels!.deepCopy() - ..selectedModel = model; - _availableModels = updatedModels; - onAvailableModelsChanged?.call(updatedModels); - } - if (model.isLocal && _isDesktop) { - _loadLocalAIState(); - } else { - _notifyStateChanged(); - } - }, - ); - } - - Future stop() async { - onChanged = null; - await _localAIListener?.stop(); - await _aiModelSwitchListener.stop(); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart new file mode 100644 index 0000000000..066280e3db --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -0,0 +1,174 @@ +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:universal_platform/universal_platform.dart'; + +typedef OnModelStateChangedCallback = void Function(AiType, bool, String); +typedef OnAvailableModelsChangedCallback = void Function( + List, + AiModel?, +); + +class AIModelStateNotifier { + AIModelStateNotifier({required this.objectId}) + : _localAIListener = + UniversalPlatform.isDesktop ? LocalAIStateListener() : null, + _aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) { + _startListening(); + _init(); + } + + final String objectId; + final LocalAIStateListener? _localAIListener; + final AIModelSwitchListener _aiModelSwitchListener; + LocalAIPB? _localAIState; + AvailableModelsPB? _availableModels; + + // callbacks + final List _stateChangedCallbacks = []; + final List + _availableModelsChangedCallbacks = []; + + void _startListening() { + if (UniversalPlatform.isDesktop) { + _localAIListener?.start( + stateCallback: (state) async { + _localAIState = state; + _notifyStateChanged(); + + if (state.state == RunningStatePB.Running || + state.state == RunningStatePB.Stopped) { + await _loadAvailableModels(); + _notifyAvailableModelsChanged(); + } + }, + ); + } + + _aiModelSwitchListener.start( + onUpdateSelectedModel: (model) async { + final updatedModels = _availableModels?.deepCopy() + ?..selectedModel = model; + _availableModels = updatedModels; + _notifyAvailableModelsChanged(); + + if (model.isLocal && UniversalPlatform.isDesktop) { + await _loadLocalAiState(); + } + _notifyStateChanged(); + }, + ); + } + + void _init() async { + await Future.wait([_loadLocalAiState(), _loadAvailableModels()]); + _notifyStateChanged(); + _notifyAvailableModelsChanged(); + } + + void addListener({ + OnModelStateChangedCallback? onStateChanged, + OnAvailableModelsChangedCallback? onAvailableModelsChanged, + }) { + if (onStateChanged != null) { + _stateChangedCallbacks.add(onStateChanged); + } + if (onAvailableModelsChanged != null) { + _availableModelsChangedCallbacks.add(onAvailableModelsChanged); + } + } + + void removeListener({ + OnModelStateChangedCallback? onStateChanged, + OnAvailableModelsChangedCallback? onAvailableModelsChanged, + }) { + if (onStateChanged != null) { + _stateChangedCallbacks.remove(onStateChanged); + } + if (onAvailableModelsChanged != null) { + _availableModelsChangedCallbacks.remove(onAvailableModelsChanged); + } + } + + Future dispose() async { + _stateChangedCallbacks.clear(); + _availableModelsChangedCallbacks.clear(); + await _localAIListener?.stop(); + await _aiModelSwitchListener.stop(); + } + + (AiType, String, bool) getState() { + if (UniversalPlatform.isMobile) { + return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); + } + + final availableModels = _availableModels; + final localAiState = _localAIState; + + if (availableModels == null) { + Log.warn("No available models"); + return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); + } + if (localAiState == null) { + Log.warn("Cannot get local AI state"); + return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); + } + + if (availableModels.selectedModel.isLocal == false) { + return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); + } + + final editable = localAiState.state == RunningStatePB.Running; + final hintText = editable + ? LocaleKeys.chat_inputLocalAIMessageHint.tr() + : LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); + + return (AiType.local, hintText, editable); + } + + (List, AiModel?) getAvailableModels() { + final availableModels = _availableModels; + if (availableModels == null) { + return ([], null); + } + final models = availableModels.models.map(AiModel.fromPB).toList(); + final selectedModel = AiModel.fromPB(availableModels.selectedModel); + return (models, selectedModel); + } + + void _notifyAvailableModelsChanged() { + final (models, selectedModel) = getAvailableModels(); + for (final callback in _availableModelsChangedCallbacks) { + callback(models, selectedModel); + } + } + + void _notifyStateChanged() { + final (type, hintText, isEditable) = getState(); + for (final callback in _stateChangedCallbacks) { + callback(type, isEditable, hintText); + } + } + + Future _loadAvailableModels() { + final payload = AvailableModelsQueryPB(source: objectId); + return AIEventGetAvailableModels(payload).send().fold( + (models) => _availableModels = models, + (err) => Log.error("Failed to get available models: $err"), + ); + } + + Future _loadLocalAiState() { + return AIEventGetLocalAIState().send().fold( + (localAIState) => _localAIState = localAIState, + (error) => Log.error("Failed to get local AI state: $error"), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart index e2acda9a1e..95854ab047 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:appflowy/ai/service/ai_input_control.dart'; +import 'package:appflowy/ai/service/ai_model_state_notifier.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,17 +14,18 @@ class AIPromptInputBloc extends Bloc { AIPromptInputBloc({ required String objectId, required PredefinedFormat? predefinedFormat, - }) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), - super(AIPromptInputState.initial(objectId, predefinedFormat)) { + }) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), + super(AIPromptInputState.initial(predefinedFormat)) { _dispatch(); + _startListening(); _init(); } - final AIModelStateNotifier _aiModelStateNotifier; + final AIModelStateNotifier aiModelStateNotifier; @override Future close() async { - await _aiModelStateNotifier.stop(); + await aiModelStateNotifier.dispose(); return super.close(); } @@ -36,7 +37,6 @@ class AIPromptInputBloc extends Bloc { emit( state.copyWith( aiType: aiType, - supportChatWithFile: false, editable: editable, hintText: hintText, ), @@ -103,16 +103,17 @@ class AIPromptInputBloc extends Bloc { ); } - void _init() { - _aiModelStateNotifier.startListening( - onChanged: (aiType, editable, hintText) { - if (!isClosed) { - add(AIPromptInputEvent.updateAIState(aiType, editable, hintText)); - } + void _startListening() { + aiModelStateNotifier.addListener( + onStateChanged: (aiType, editable, hintText) { + add(AIPromptInputEvent.updateAIState(aiType, editable, hintText)); }, ); + } - _aiModelStateNotifier.init(); + void _init() { + final (aiType, hintText, isEditable) = aiModelStateNotifier.getState(); + add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText)); } Map consumeMetadata() { @@ -155,7 +156,6 @@ class AIPromptInputEvent with _$AIPromptInputEvent { @freezed class AIPromptInputState with _$AIPromptInputState { const factory AIPromptInputState({ - required String objectId, required AiType aiType, required bool supportChatWithFile, required bool showPredefinedFormats, @@ -166,12 +166,8 @@ class AIPromptInputState with _$AIPromptInputState { required String hintText, }) = _AIPromptInputState; - factory AIPromptInputState.initial( - String objectId, - PredefinedFormat? format, - ) => + factory AIPromptInputState.initial(PredefinedFormat? format) => AIPromptInputState( - objectId: objectId, aiType: AiType.cloud, supportChatWithFile: false, showPredefinedFormats: format != null, @@ -182,11 +178,3 @@ class AIPromptInputState with _$AIPromptInputState { hintText: '', ); } - -enum AiType { - cloud, - local; - - bool get isCloud => this == cloud; - bool get isLocal => this == local; -} diff --git a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart index 665533bd40..46e9b5ef92 100644 --- a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart @@ -1,82 +1,110 @@ import 'dart:async'; -import 'package:appflowy/ai/service/ai_input_control.dart'; +import 'package:appflowy/ai/service/ai_model_state_notifier.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; part 'select_model_bloc.freezed.dart'; +class AiModel extends Equatable { + const AiModel({ + required this.name, + required this.isLocal, + }); + + factory AiModel.fromPB(AIModelPB pb) { + return AiModel(name: pb.name, isLocal: pb.isLocal); + } + + AIModelPB toPB() { + return AIModelPB() + ..name = name + ..isLocal = isLocal; + } + + final String name; + final bool isLocal; + + @override + List get props => [name, isLocal]; +} + class SelectModelBloc extends Bloc { SelectModelBloc({ - required this.objectId, - }) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), - super(const SelectModelState()) { - _aiModelStateNotifier.init(); - _aiModelStateNotifier.startListening( - onAvailableModelsChanged: (models) { - if (!isClosed) { - add(SelectModelEvent.didLoadModels(models)); - } - }, - ); - + required AIModelStateNotifier aiModelStateNotifier, + }) : _aiModelStateNotifier = aiModelStateNotifier, + super(SelectModelState.initial()) { on( - (event, emit) async { - await event.when( - selectModel: (AIModelPB model) async { - await AIEventUpdateSelectedModel( + (event, emit) { + event.when( + selectModel: (model) { + AIEventUpdateSelectedModel( UpdateSelectedModelPB( - source: objectId, - selectedModel: model, + source: _aiModelStateNotifier.objectId, + selectedModel: model.toPB(), ), ).send(); - state.availableModels?.freeze(); - final newAvailableModels = state.availableModels?.rebuild((m) { - m.selectedModel = model; - }); - + emit(state.copyWith(selectedModel: model)); + }, + didLoadModels: (models, selectedModel) { emit( - state.copyWith( - availableModels: newAvailableModels, + SelectModelState( + models: models, + selectedModel: selectedModel, ), ); }, - didLoadModels: (AvailableModelsPB models) { - emit(state.copyWith(availableModels: models)); - }, ); }, ); + + _aiModelStateNotifier.addListener( + onAvailableModelsChanged: _onAvailableModelsChanged, + ); } - final String objectId; final AIModelStateNotifier _aiModelStateNotifier; @override Future close() async { - await _aiModelStateNotifier.stop(); + _aiModelStateNotifier.removeListener( + onAvailableModelsChanged: _onAvailableModelsChanged, + ); await super.close(); } + + void _onAvailableModelsChanged(List models, AiModel? selectedModel) { + if (!isClosed) { + add(SelectModelEvent.didLoadModels(models, selectedModel)); + } + } } @freezed class SelectModelEvent with _$SelectModelEvent { const factory SelectModelEvent.selectModel( - AIModelPB model, + AiModel model, ) = _SelectModel; const factory SelectModelEvent.didLoadModels( - AvailableModelsPB models, + List models, + AiModel? selectedModel, ) = _DidLoadModels; } @freezed class SelectModelState with _$SelectModelState { const factory SelectModelState({ - AvailableModelsPB? availableModels, + required List models, + required AiModel? selectedModel, }) = _SelectModelState; + + factory SelectModelState.initial() => SelectModelState( + models: [], + selectedModel: null, + ); } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index bf21a59ad0..fcf487da94 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -1,17 +1,14 @@ import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/ai/service/select_model_bloc.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:extended_text_field/extended_text_field.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'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -169,7 +166,6 @@ class _DesktopPromptInputState extends State { top: null, child: TextFieldTapRegion( child: _PromptBottomActions( - objectId: state.objectId, showPredefinedFormats: state.showPredefinedFormats, onTogglePredefinedFormatSection: () => @@ -567,7 +563,6 @@ class PromptInputTextField extends StatelessWidget { class _PromptBottomActions extends StatelessWidget { const _PromptBottomActions({ - required this.objectId, required this.sendButtonState, required this.showPredefinedFormats, required this.onTogglePredefinedFormatSection, @@ -579,7 +574,6 @@ class _PromptBottomActions extends StatelessWidget { this.extraBottomActionButton, }); - final String objectId; final bool showPredefinedFormats; final void Function() onTogglePredefinedFormatSection; final void Function() onStartMention; @@ -600,10 +594,16 @@ class _PromptBottomActions extends StatelessWidget { return Row( children: [ _predefinedFormatButton(), - SelectModelButton(objectId: objectId), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + SelectModelMenu( + aiModelStateNotifier: + context.read().aiModelStateNotifier, + ), const Spacer(), if (state.aiType.isCloud) ...[ - _selectSourcesButton(context), + _selectSourcesButton(), const HSpace( DesktopAIChatSizes.inputActionBarButtonSpacing, ), @@ -639,7 +639,7 @@ class _PromptBottomActions extends StatelessWidget { ); } - Widget _selectSourcesButton(BuildContext context) { + Widget _selectSourcesButton() { return PromptInputDesktopSelectSourcesButton( onUpdateSelectedSources: onUpdateSelectedSources, selectedSourcesNotifier: selectedSourcesNotifier, @@ -686,225 +686,3 @@ class _PromptBottomActions extends StatelessWidget { ); } } - -class SelectModelButton extends StatefulWidget { - const SelectModelButton({ - super.key, - required this.objectId, - }); - - final String objectId; - - @override - State createState() => _SelectModelButtonState(); -} - -class _SelectModelButtonState extends State { - final popoverController = PopoverController(); - late SelectModelBloc bloc; - - @override - void initState() { - super.initState(); - bloc = SelectModelBloc(objectId: widget.objectId); - } - - @override - void dispose() { - popoverController.close(); - bloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - return AppFlowyPopover( - // constraints: BoxConstraints.loose(const Size(250, 200)), - offset: const Offset(0.0, -10.0), - direction: PopoverDirection.topWithLeftAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () {}, - onClose: () {}, - popupBuilder: (_) { - return BlocProvider.value( - value: bloc, - child: _PopoverSelectModel( - onClose: () => popoverController.close(), - ), - ); - }, - child: _CurrentModelButton( - key: ValueKey(state.availableModels?.selectedModel.name), - modelName: state.availableModels?.selectedModel.name ?? "", - onTap: () => popoverController.show(), - ), - ); - }, - ), - ); - } -} - -class _PopoverSelectModel extends StatelessWidget { - const _PopoverSelectModel({ - required this.onClose, - }); - - final VoidCallback onClose; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.availableModels == null || - state.availableModels!.models.isEmpty) { - return const SizedBox.shrink(); - } - - // Separate models into local and cloud models - final localModels = state.availableModels!.models - .where((model) => model.isLocal) - .toList(); - - final cloudModels = state.availableModels!.models - .where((model) => !model.isLocal) - .toList(); - - return Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Local AI Models Section - if (localModels.isNotEmpty) ...[ - _ModelSectionHeader( - title: LocaleKeys.chat_changeFormat_localModel.tr(), - ), - const SizedBox(height: 4), - ...localModels.map( - (model) => _ModelItem( - model: model, - onTap: () { - context.read().add( - SelectModelEvent.selectModel(model), - ); - onClose(); - }, - ), - ), - const SizedBox(height: 8), - ], - - // Cloud AI Models Section - if (cloudModels.isNotEmpty) ...[ - if (localModels.isNotEmpty) - _ModelSectionHeader( - title: LocaleKeys.chat_changeFormat_cloudModel.tr(), - ), - const VSpace(4), - ...cloudModels.map( - (model) => _ModelItem( - model: model, - onTap: () { - context.read().add( - SelectModelEvent.selectModel(model), - ); - onClose(); - }, - ), - ), - ], - ], - ), - ); - }, - ); - } -} - -class _ModelSectionHeader extends StatelessWidget { - const _ModelSectionHeader({ - required this.title, - }); - - final String title; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 2), - child: FlowyText( - title, - fontSize: 12, - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w500, - ), - ); - } -} - -class _ModelItem extends StatelessWidget { - const _ModelItem({ - required this.model, - required this.onTap, - }); - - final AIModelPB model; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final modelName = model.name; - - return FlowyTextButton( - modelName, - fillColor: Colors.transparent, - onPressed: onTap, - ); - } -} - -class _CurrentModelButton extends StatelessWidget { - const _CurrentModelButton({ - required this.modelName, - required this.onTap, - super.key, - }); - - final String modelName; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_changeFormat_switchModel.tr(), - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), - child: FlowyText( - modelName, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - ), - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart index 6d6fc8de31..403b978905 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -104,6 +104,7 @@ class ChangeFormatBar extends StatelessWidget { }, child: FlowyTooltip( message: format.i18n, + preferBelow: false, child: SizedBox.square( dimension: _buttonSize, child: FlowyHover( @@ -150,6 +151,7 @@ class ChangeFormatBar extends StatelessWidget { }, child: FlowyTooltip( message: format.i18n, + preferBelow: false, child: SizedBox.square( dimension: _buttonSize, child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart new file mode 100644 index 0000000000..d6ade94cc5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -0,0 +1,221 @@ +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +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:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SelectModelMenu extends StatefulWidget { + const SelectModelMenu({ + super.key, + required this.aiModelStateNotifier, + }); + + final AIModelStateNotifier aiModelStateNotifier; + + @override + State createState() => _SelectModelMenuState(); +} + +class _SelectModelMenuState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SelectModelBloc( + aiModelStateNotifier: widget.aiModelStateNotifier, + ), + child: BlocBuilder( + builder: (context, state) { + if (state.selectedModel == null) { + return const SizedBox.shrink(); + } + return AppFlowyPopover( + offset: Offset(-12.0, 0.0), + constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), + direction: PopoverDirection.topWithLeftAligned, + margin: EdgeInsets.zero, + controller: popoverController, + popupBuilder: (popoverContext) { + return SelectModelPopoverContent( + models: state.models, + selectedModel: state.selectedModel, + onSelectModel: (model) { + if (model != state.selectedModel) { + context + .read() + .add(SelectModelEvent.selectModel(model)); + } + popoverController.close(); + }, + ); + }, + child: _CurrentModelButton( + modelName: state.selectedModel!.name, + onTap: () => popoverController.show(), + ), + ); + }, + ), + ); + } +} + +class SelectModelPopoverContent extends StatelessWidget { + const SelectModelPopoverContent({ + super.key, + required this.models, + required this.selectedModel, + this.onSelectModel, + }); + + final List models; + final AiModel? selectedModel; + final void Function(AiModel)? onSelectModel; + + @override + Widget build(BuildContext context) { + if (models.isEmpty) { + return const SizedBox.shrink(); + } + + // separate models into local and cloud models + final localModels = models.where((model) => model.isLocal).toList(); + final cloudModels = models.where((model) => !model.isLocal).toList(); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (localModels.isNotEmpty) ...[ + _ModelSectionHeader( + title: LocaleKeys.chat_switchModel_localModel.tr(), + ), + const VSpace(4.0), + ], + ...localModels.map( + (model) => _ModelItem( + model: model, + isSelected: model == selectedModel, + onTap: () => onSelectModel?.call(model), + ), + ), + if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[ + const VSpace(8.0), + _ModelSectionHeader( + title: LocaleKeys.chat_switchModel_cloudModel.tr(), + ), + const VSpace(4.0), + ], + ...cloudModels.map( + (model) => _ModelItem( + model: model, + isSelected: model == selectedModel, + onTap: () => onSelectModel?.call(model), + ), + ), + ], + ), + ); + } +} + +class _ModelSectionHeader extends StatelessWidget { + const _ModelSectionHeader({ + required this.title, + }); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 4, bottom: 2), + child: FlowyText( + title, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w500, + ), + ); + } +} + +class _ModelItem extends StatelessWidget { + const _ModelItem({ + required this.model, + required this.isSelected, + required this.onTap, + }); + + final AiModel model; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: FlowyButton( + onTap: onTap, + margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + text: FlowyText(model.name), + rightIcon: isSelected ? FlowySvg(FlowySvgs.check_s) : null, + ), + ); + } +} + +class _CurrentModelButton extends StatelessWidget { + const _CurrentModelButton({ + required this.modelName, + required this.onTap, + }); + + final String modelName; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.chat_switchModel_label.tr(), + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: DesktopAIPromptSizes.actionBarButtonSize, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(4.0), + child: Row( + children: [ + FlowyText( + modelName, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + ), + HSpace(2.0), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index e7aca346e0..f8a1b4d20c 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -239,9 +239,9 @@ class ChatBloc extends Bloc { ), ); }, - regenerateAnswer: (id, format) { + regenerateAnswer: (id, format, model) { _clearRelatedQuestions(); - _regenerateAnswer(id, format); + _regenerateAnswer(id, format, model); lastSentMessage = null; isFetchingRelatedQuestions = false; @@ -483,6 +483,7 @@ class ChatBloc extends Bloc { void _regenerateAnswer( String answerMessageIdString, PredefinedFormat? format, + AiModel? model, ) async { final id = temporaryMessageIDMap.entries .firstWhereOrNull((e) => e.value == answerMessageIdString) @@ -505,6 +506,9 @@ class ChatBloc extends Bloc { if (format != null) { payload.format = format.toPB(); } + if (model != null) { + payload.model = model.toPB(); + } await AIEventRegenerateResponse(payload).send().fold( (success) { @@ -637,6 +641,7 @@ class ChatEvent with _$ChatEvent { const factory ChatEvent.regenerateAnswer( String id, PredefinedFormat? format, + AiModel? model, ) = _RegenerateAnswer; // streaming answer diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index cbc4929f56..4f843d447b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -265,10 +265,13 @@ class _ChatContentPage extends StatelessWidget { _onSelectMetadata(context, metadata), onRegenerate: () => context .read() - .add(ChatEvent.regenerateAnswer(message.id, null)), + .add(ChatEvent.regenerateAnswer(message.id, null, null)), onChangeFormat: (format) => context .read() - .add(ChatEvent.regenerateAnswer(message.id, format)), + .add(ChatEvent.regenerateAnswer(message.id, format, null)), + onChangeModel: (model) => context + .read() + .add(ChatEvent.regenerateAnswer(message.id, null, model)), onStopStream: () => context.read().add( const ChatEvent.stopStream(), ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart new file mode 100644 index 0000000000..2bf09d5e3c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart @@ -0,0 +1,145 @@ +import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +Future showChangeModelBottomSheet( + BuildContext context, + List models, +) { + return showMobileBottomSheet( + context, + showDragHandle: true, + builder: (context) => _ChangeModelBottomSheetContent(models: models), + ); +} + +class _ChangeModelBottomSheetContent extends StatefulWidget { + const _ChangeModelBottomSheetContent({ + required this.models, + }); + + final List models; + + @override + State<_ChangeModelBottomSheetContent> createState() => + _ChangeModelBottomSheetContentState(); +} + +class _ChangeModelBottomSheetContentState + extends State<_ChangeModelBottomSheetContent> { + AiModel? model; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _Header( + onCancel: () => Navigator.of(context).pop(), + onDone: () => Navigator.of(context).pop(model), + ), + const VSpace(4.0), + _Body( + models: widget.models, + selectedModel: model, + onSelectModel: (format) { + setState(() => model = format); + }, + ), + const VSpace(16.0), + ], + ); + } +} + +class _Header extends StatelessWidget { + const _Header({ + required this.onCancel, + required this.onDone, + }); + + final VoidCallback onCancel; + final VoidCallback onDone; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44.0, + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: AppBarBackButton( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + onTap: onCancel, + ), + ), + Align( + child: Container( + constraints: const BoxConstraints(maxWidth: 250), + child: FlowyText( + LocaleKeys.chat_switchModel_label.tr(), + fontSize: 17.0, + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: AppBarDoneButton( + onTap: onDone, + ), + ), + ], + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + required this.models, + required this.selectedModel, + required this.onSelectModel, + }); + + final List models; + final AiModel? selectedModel; + final void Function(AiModel) onSelectModel; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: models + .mapIndexed( + (index, model) => _buildModelButton(model, index == 0), + ) + .toList(), + ); + } + + Widget _buildModelButton( + AiModel model, [ + bool isFirst = false, + ]) { + return FlowyOptionTile.checkbox( + text: model.name, + isSelected: model == selectedModel, + showTopBorder: isFirst, + onTap: () { + onSelectModel(model); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 6b1d428d04..8649e08a5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -41,6 +41,7 @@ class AIMessageActionBar extends StatefulWidget { required this.showDecoration, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, this.onOverrideVisibility, }); @@ -48,6 +49,7 @@ class AIMessageActionBar extends StatefulWidget { final bool showDecoration; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; final void Function(bool)? onOverrideVisibility; @override @@ -126,6 +128,12 @@ class _AIMessageActionBarState extends State { popoverMutex: popoverMutex, onOverrideVisibility: widget.onOverrideVisibility, ), + ChangeModelButton( + isInHoverBar: widget.showDecoration, + onRegenerate: widget.onChangeModel, + popoverMutex: popoverMutex, + onOverrideVisibility: widget.onOverrideVisibility, + ), SaveToPageButton( textMessage: widget.message as TextMessage, isInHoverBar: widget.showDecoration, @@ -405,6 +413,85 @@ class _ChangeFormatPopoverContentState } } +class ChangeModelButton extends StatefulWidget { + const ChangeModelButton({ + super.key, + required this.isInHoverBar, + this.popoverMutex, + this.onRegenerate, + this.onOverrideVisibility, + }); + + final bool isInHoverBar; + final PopoverMutex? popoverMutex; + final void Function(AiModel)? onRegenerate; + final void Function(bool)? onOverrideVisibility; + + @override + State createState() => _ChangeModelButtonState(); +} + +class _ChangeModelButtonState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + mutex: widget.popoverMutex, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + offset: Offset(8, 0), + direction: PopoverDirection.rightWithBottomAligned, + constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), + onClose: () => widget.onOverrideVisibility?.call(false), + child: buildButton(context), + popupBuilder: (_) { + final bloc = context.read(); + final (models, _) = bloc.aiModelStateNotifier.getAvailableModels(); + return SelectModelPopoverContent( + models: models, + selectedModel: null, + onSelectModel: widget.onRegenerate, + ); + }, + ); + } + + Widget buildButton(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.chat_switchModel_label.tr(), + child: FlowyIconButton( + width: 32.0, + height: DesktopAIChatSizes.messageActionBarIconSize, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + radius: widget.isInHoverBar + ? DesktopAIChatSizes.messageHoverActionBarIconRadius + : DesktopAIChatSizes.messageActionBarIconRadius, + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.ai_sparks_s, + color: Theme.of(context).hintColor, + size: const Size.square(16), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), + onPressed: () { + widget.onOverrideVisibility?.call(true); + popoverController.show(); + }, + ), + ); + } +} + class SaveToPageButton extends StatefulWidget { const SaveToPageButton({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index 770fb990b1..aa61a3f621 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -23,6 +23,7 @@ import 'package:universal_platform/universal_platform.dart'; import '../chat_avatar.dart'; import '../layout_define.dart'; +import 'ai_change_model_bottom_sheet.dart'; import 'ai_message_action_bar.dart'; import 'ai_change_format_bottom_sheet.dart'; import 'message_util.dart'; @@ -41,6 +42,7 @@ class ChatAIMessageBubble extends StatelessWidget { this.isSelectingMessages = false, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, }); final Message message; @@ -50,6 +52,7 @@ class ChatAIMessageBubble extends StatelessWidget { final bool isSelectingMessages; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; @override Widget build(BuildContext context) { @@ -73,6 +76,7 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, + onChangeModel: onChangeModel, child: child, ); } @@ -82,6 +86,7 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, + onChangeModel: onChangeModel, child: child, ); } @@ -91,6 +96,7 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, + onChangeModel: onChangeModel, child: child, ); } @@ -103,12 +109,14 @@ class ChatAIBottomInlineActions extends StatelessWidget { required this.message, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, }); final Widget child; final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; @override Widget build(BuildContext context) { @@ -127,6 +135,7 @@ class ChatAIBottomInlineActions extends StatelessWidget { showDecoration: false, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, + onChangeModel: onChangeModel, ), ), const VSpace(32.0), @@ -142,12 +151,14 @@ class ChatAIMessageHover extends StatefulWidget { required this.message, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, }); final Widget child; final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; @override State createState() => _ChatAIMessageHoverState(); @@ -229,6 +240,7 @@ class _ChatAIMessageHoverState extends State { showDecoration: true, onRegenerate: widget.onRegenerate, onChangeFormat: widget.onChangeFormat, + onChangeModel: widget.onChangeModel, onOverrideVisibility: (visibility) { overrideVisibility = visibility; }, @@ -302,12 +314,14 @@ class ChatAIMessagePopup extends StatelessWidget { required this.message, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, }); final Widget child; final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; @override Widget build(BuildContext context) { @@ -328,6 +342,8 @@ class ChatAIMessagePopup extends StatelessWidget { _divider(), _changeFormatButton(context), _divider(), + _changeModelButton(context), + _divider(), _saveToPageButton(context), ], ); @@ -399,6 +415,25 @@ class ChatAIMessagePopup extends StatelessWidget { ); } + Widget _changeModelButton(BuildContext context) { + return MobileQuickActionButton( + onTap: () async { + final bloc = context.read(); + final (models, _) = bloc.aiModelStateNotifier.getAvailableModels(); + final result = await showChangeModelBottomSheet(context, models); + if (result != null) { + onChangeModel?.call(result); + if (context.mounted) { + Navigator.of(context).pop(); + } + } + }, + icon: FlowySvgs.ai_sparks_s, + iconSize: const Size.square(20), + text: LocaleKeys.chat_switchModel_label.tr(), + ); + } + Widget _saveToPageButton(BuildContext context) { return MobileQuickActionButton( onTap: () async { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index 5a55072c17..e181de6570 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -36,6 +36,7 @@ class ChatAIMessageWidget extends StatelessWidget { this.onSelectedMetadata, this.onRegenerate, this.onChangeFormat, + this.onChangeModel, this.isLastMessage = false, this.isStreaming = false, this.isSelectingMessages = false, @@ -53,6 +54,7 @@ class ChatAIMessageWidget extends StatelessWidget { final void Function()? onRegenerate; final void Function() onStopStream; final void Function(PredefinedFormat)? onChangeFormat; + final void Function(AiModel)? onChangeModel; final bool isStreaming; final bool isLastMessage; final bool isSelectingMessages; @@ -110,6 +112,7 @@ class ChatAIMessageWidget extends StatelessWidget { isSelectingMessages: isSelectingMessages, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, + onChangeModel: onChangeModel, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 62de8be000..24f734af5d 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -247,14 +247,16 @@ "table": "Table", "blankDescription": "Format response", "defaultDescription": "Auto mode", - "localModel": "Local Model", - "cloudModel": "Cloud Model", - "switchModel": "Switch model", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", "tableWithImageDescription": "@:chat.changeFormat.table with image" }, + "switchModel": { + "label": "Switch model", + "localModel": "Local Model", + "cloudModel": "Cloud Model" + }, "selectBanner": { "saveButton": "Add to …", "selectMessages": "Select messages", @@ -3199,4 +3201,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} \ No newline at end of file +} diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 03a18dcdec..0a63d8ece0 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -248,22 +248,23 @@ impl AIManager { answer_message_id: i64, answer_stream_port: i64, format: Option, + model: Option, ) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; let question_message_id = chat .get_question_id_from_answer_id(answer_message_id) .await?; - let preferred_model = self - .store_preferences - .get_object::(&ai_available_models_key(chat_id)); + let model = model.map_or_else( + || { + self + .store_preferences + .get_object::(&ai_available_models_key(chat_id)) + }, + |model| Some(model.into()), + ); chat - .stream_regenerate_response( - question_message_id, - answer_stream_port, - format, - preferred_model, - ) + .stream_regenerate_response(question_message_id, answer_stream_port, format, model) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 0e996228c6..d10aa950e3 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -102,6 +102,9 @@ pub struct RegenerateResponsePB { #[pb(index = 4, one_of)] pub format: Option, + + #[pb(index = 5, one_of)] + pub model: Option, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 6bedebd261..ec8b7b4964 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -99,6 +99,7 @@ pub(crate) async fn regenerate_response_handler( data.answer_message_id, data.answer_stream_port, data.format, + data.model, ) .await?; Ok(()) From e11388491f7a7730fdcf66721770506e498ff309 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:59:20 +0800 Subject: [PATCH 221/384] chore: emit ready state faster --- .../lib/ai/service/select_model_bloc.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart index 46e9b5ef92..c6c6c03ccf 100644 --- a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart @@ -36,7 +36,7 @@ class SelectModelBloc extends Bloc { SelectModelBloc({ required AIModelStateNotifier aiModelStateNotifier, }) : _aiModelStateNotifier = aiModelStateNotifier, - super(SelectModelState.initial()) { + super(SelectModelState.initial(aiModelStateNotifier)) { on( (event, emit) { event.when( @@ -103,8 +103,11 @@ class SelectModelState with _$SelectModelState { required AiModel? selectedModel, }) = _SelectModelState; - factory SelectModelState.initial() => SelectModelState( - models: [], - selectedModel: null, - ); + factory SelectModelState.initial(AIModelStateNotifier notifier) { + final (models, selectedModel) = notifier.getAvailableModels(); + return SelectModelState( + models: models, + selectedModel: selectedModel, + ); + } } From f9e1dcca6ca41a8bd64d1c30b9dda4a0ad48dabe Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:57:45 +0800 Subject: [PATCH 222/384] chore: code nit --- .../lib/ai/service/ai_model_state_notifier.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index 066280e3db..a94c863acd 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -121,7 +121,7 @@ class AIModelStateNotifier { return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); } - if (availableModels.selectedModel.isLocal == false) { + if (!availableModels.selectedModel.isLocal) { return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); } From f7f2e71ee15f649f23926fd0b79ab8848d12bb21 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:05:26 +0800 Subject: [PATCH 223/384] chore: adjust popover shadows (#7630) --- .../lib/src/flowy_overlay/appflowy_popover.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 2da547afda..6a154d4d48 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -14,10 +14,10 @@ class ShadowConstants { BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x121F2225)), ]; static const List darkSmall = [ - BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x5C000000)), + BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x7A000000)), ]; static const List darkMedium = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x5C000000)), + BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x7A000000)), ]; } From 9147f64b652fdddf32fba946f615a585878684e9 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:10:23 +0800 Subject: [PATCH 224/384] chore: adjust suggestion action button position (#7632) --- .../editor_plugins/ai/ai_writer_block_component.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index a4bc4f50e3..0f1e077d6d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -222,9 +222,11 @@ class _OverlayContentState extends State { final isInitialReadyState = state is ReadyAiWriterState && state.isFirstRun; final showSuggestedActionsPopup = - showSuggestedActions && markdownText.isEmpty; - final showSuggestedActionsWithin = - showSuggestedActions && markdownText.isNotEmpty; + showSuggestedActions && markdownText.isEmpty || + (markdownText.isNotEmpty && command != AiWriterCommand.explain); + final showSuggestedActionsWithin = showSuggestedActions && + markdownText.isNotEmpty && + command == AiWriterCommand.explain; final borderColor = Theme.of(context).isLightMode ? Color(0x1F1F2329) From 85288119924ba61eb5743b588c4b351bdc7dbe2f Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:02:58 +0800 Subject: [PATCH 225/384] chore: remove ai model class --- .../ai/service/ai_model_state_notifier.dart | 10 ++--- .../lib/ai/service/select_model_bloc.dart | 41 +++++-------------- .../prompt_input/select_model_menu.dart | 9 ++-- .../ai_chat/application/chat_bloc.dart | 6 +-- .../message/ai_change_model_bottom_sheet.dart | 20 ++++----- .../message/ai_message_action_bar.dart | 5 ++- .../message/ai_message_bubble.dart | 9 ++-- .../presentation/message/ai_text_message.dart | 3 +- 8 files changed, 42 insertions(+), 61 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index a94c863acd..c43bd01c6f 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -12,8 +12,8 @@ import 'package:universal_platform/universal_platform.dart'; typedef OnModelStateChangedCallback = void Function(AiType, bool, String); typedef OnAvailableModelsChangedCallback = void Function( - List, - AiModel?, + List, + AIModelPB?, ); class AIModelStateNotifier { @@ -133,14 +133,12 @@ class AIModelStateNotifier { return (AiType.local, hintText, editable); } - (List, AiModel?) getAvailableModels() { + (List, AIModelPB?) getAvailableModels() { final availableModels = _availableModels; if (availableModels == null) { return ([], null); } - final models = availableModels.models.map(AiModel.fromPB).toList(); - final selectedModel = AiModel.fromPB(availableModels.selectedModel); - return (models, selectedModel); + return (availableModels.models, availableModels.selectedModel); } void _notifyAvailableModelsChanged() { diff --git a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart index c6c6c03ccf..7ad52b9ec4 100644 --- a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart @@ -3,35 +3,11 @@ import 'dart:async'; import 'package:appflowy/ai/service/ai_model_state_notifier.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'select_model_bloc.freezed.dart'; -class AiModel extends Equatable { - const AiModel({ - required this.name, - required this.isLocal, - }); - - factory AiModel.fromPB(AIModelPB pb) { - return AiModel(name: pb.name, isLocal: pb.isLocal); - } - - AIModelPB toPB() { - return AIModelPB() - ..name = name - ..isLocal = isLocal; - } - - final String name; - final bool isLocal; - - @override - List get props => [name, isLocal]; -} - class SelectModelBloc extends Bloc { SelectModelBloc({ required AIModelStateNotifier aiModelStateNotifier, @@ -44,7 +20,7 @@ class SelectModelBloc extends Bloc { AIEventUpdateSelectedModel( UpdateSelectedModelPB( source: _aiModelStateNotifier.objectId, - selectedModel: model.toPB(), + selectedModel: model, ), ).send(); @@ -77,7 +53,10 @@ class SelectModelBloc extends Bloc { await super.close(); } - void _onAvailableModelsChanged(List models, AiModel? selectedModel) { + void _onAvailableModelsChanged( + List models, + AIModelPB? selectedModel, + ) { if (!isClosed) { add(SelectModelEvent.didLoadModels(models, selectedModel)); } @@ -87,20 +66,20 @@ class SelectModelBloc extends Bloc { @freezed class SelectModelEvent with _$SelectModelEvent { const factory SelectModelEvent.selectModel( - AiModel model, + AIModelPB model, ) = _SelectModel; const factory SelectModelEvent.didLoadModels( - List models, - AiModel? selectedModel, + List models, + AIModelPB? selectedModel, ) = _DidLoadModels; } @freezed class SelectModelState with _$SelectModelState { const factory SelectModelState({ - required List models, - required AiModel? selectedModel, + required List models, + required AIModelPB? selectedModel, }) = _SelectModelState; factory SelectModelState.initial(AIModelStateNotifier notifier) { diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index d6ade94cc5..b9e3daada9 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -1,6 +1,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.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'; @@ -72,9 +73,9 @@ class SelectModelPopoverContent extends StatelessWidget { this.onSelectModel, }); - final List models; - final AiModel? selectedModel; - final void Function(AiModel)? onSelectModel; + final List models; + final AIModelPB? selectedModel; + final void Function(AIModelPB)? onSelectModel; @override Widget build(BuildContext context) { @@ -154,7 +155,7 @@ class _ModelItem extends StatelessWidget { required this.onTap, }); - final AiModel model; + final AIModelPB model; final bool isSelected; final VoidCallback onTap; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index f8a1b4d20c..4924a42c0d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -483,7 +483,7 @@ class ChatBloc extends Bloc { void _regenerateAnswer( String answerMessageIdString, PredefinedFormat? format, - AiModel? model, + AIModelPB? model, ) async { final id = temporaryMessageIDMap.entries .firstWhereOrNull((e) => e.value == answerMessageIdString) @@ -507,7 +507,7 @@ class ChatBloc extends Bloc { payload.format = format.toPB(); } if (model != null) { - payload.model = model.toPB(); + payload.model = model; } await AIEventRegenerateResponse(payload).send().fold( @@ -641,7 +641,7 @@ class ChatEvent with _$ChatEvent { const factory ChatEvent.regenerateAnswer( String id, PredefinedFormat? format, - AiModel? model, + AIModelPB? model, ) = _RegenerateAnswer; // streaming answer diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart index 2bf09d5e3c..aa0d840574 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart @@ -1,18 +1,18 @@ -import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -Future showChangeModelBottomSheet( +Future showChangeModelBottomSheet( BuildContext context, - List models, + List models, ) { - return showMobileBottomSheet( + return showMobileBottomSheet( context, showDragHandle: true, builder: (context) => _ChangeModelBottomSheetContent(models: models), @@ -24,7 +24,7 @@ class _ChangeModelBottomSheetContent extends StatefulWidget { required this.models, }); - final List models; + final List models; @override State<_ChangeModelBottomSheetContent> createState() => @@ -33,7 +33,7 @@ class _ChangeModelBottomSheetContent extends StatefulWidget { class _ChangeModelBottomSheetContentState extends State<_ChangeModelBottomSheetContent> { - AiModel? model; + AIModelPB? model; @override Widget build(BuildContext context) { @@ -113,9 +113,9 @@ class _Body extends StatelessWidget { required this.onSelectModel, }); - final List models; - final AiModel? selectedModel; - final void Function(AiModel) onSelectModel; + final List models; + final AIModelPB? selectedModel; + final void Function(AIModelPB) onSelectModel; @override Widget build(BuildContext context) { @@ -130,7 +130,7 @@ class _Body extends StatelessWidget { } Widget _buildModelButton( - AiModel model, [ + AIModelPB model, [ bool isFirst = false, ]) { return FlowyOptionTile.checkbox( diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 8649e08a5c..150ce20192 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -21,6 +21,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -49,7 +50,7 @@ class AIMessageActionBar extends StatefulWidget { final bool showDecoration; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; final void Function(bool)? onOverrideVisibility; @override @@ -424,7 +425,7 @@ class ChangeModelButton extends StatefulWidget { final bool isInHoverBar; final PopoverMutex? popoverMutex; - final void Function(AiModel)? onRegenerate; + final void Function(AIModelPB)? onRegenerate; final void Function(bool)? onOverrideVisibility; @override diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index aa61a3f621..eed5f0a520 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -12,6 +12,7 @@ import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -52,7 +53,7 @@ class ChatAIMessageBubble extends StatelessWidget { final bool isSelectingMessages; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; @override Widget build(BuildContext context) { @@ -116,7 +117,7 @@ class ChatAIBottomInlineActions extends StatelessWidget { final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; @override Widget build(BuildContext context) { @@ -158,7 +159,7 @@ class ChatAIMessageHover extends StatefulWidget { final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; @override State createState() => _ChatAIMessageHoverState(); @@ -321,7 +322,7 @@ class ChatAIMessagePopup extends StatelessWidget { final Message message; final void Function()? onRegenerate; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index e181de6570..380767105f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -54,7 +55,7 @@ class ChatAIMessageWidget extends StatelessWidget { final void Function()? onRegenerate; final void Function() onStopStream; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AiModel)? onChangeModel; + final void Function(AIModelPB)? onChangeModel; final bool isStreaming; final bool isLastMessage; final bool isSelectingMessages; From 4686e13390d66ab497f7cdf51a1dad2a9c0c011c Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 27 Mar 2025 13:28:35 +0800 Subject: [PATCH 226/384] feat: add animation for floating toolbar (#7623) --- .../lib/plugins/document/presentation/editor_page.dart | 4 +++- .../desktop_toolbar/desktop_floating_toolbar.dart | 7 ++++++- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index b14d930d65..d0d26c43e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -443,9 +443,11 @@ class _AppFlowyEditorPageState extends State color: Theme.of(context).cardColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), - toolbarBuilder: (context, child, onDismiss) => DesktopFloatingToolbar( + toolbarBuilder: (context, child, onDismiss, isMetricsChanged) => + DesktopFloatingToolbar( editorState: editorState, onDismiss: onDismiss, + enableAnimation: !isMetricsChanged, child: child, ), placeHolderBuilder: (_) => customPlaceholderItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart index 2cc9d700e2..076358034c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -2,6 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'toolbar_animation.dart'; import 'toolbar_cubit.dart'; class DesktopFloatingToolbar extends StatefulWidget { @@ -10,11 +11,13 @@ class DesktopFloatingToolbar extends StatefulWidget { required this.editorState, required this.child, required this.onDismiss, + this.enableAnimation = true, }); final EditorState editorState; final Widget child; final VoidCallback onDismiss; + final bool enableAnimation; @override State createState() => _DesktopFloatingToolbarState(); @@ -46,7 +49,9 @@ class _DesktopFloatingToolbarState extends State { left: position!.left, top: position!.top, right: position!.right, - child: widget.child, + child: widget.enableAnimation + ? ToolbarAnimationWidget(child: widget.child) + : widget.child, ), ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index d61c5338f5..ea4cb90fe1 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -90,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: f46e991 - resolved-ref: f46e991d0a9c5a95bd14be4cc96e68171c9ed9bc + ref: "8f314fd" + resolved-ref: "8f314fda5981e650a52ba522ba7915e13940d837" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 4710d52b4d..77663760a2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -180,7 +180,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "f46e991" + ref: "8f314fd" appflowy_editor_plugins: git: From 76cb23e23358796e4d34cf8bc1be715b85db8a73 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 27 Mar 2025 14:17:47 +0800 Subject: [PATCH 227/384] feat: add bloc observer (#7633) * chore: bump version 0.8.8 * feat: add bloc observer * chore: update comment * chore: update comment --- .../callout/callout_block_component.dart | 8 +- .../simple_table_node_extension.dart | 24 +++++- .../appflowy_flutter/lib/startup/startup.dart | 2 +- .../lib/startup/tasks/app_widget.dart | 10 --- .../lib/startup/tasks/debug_task.dart | 35 +++++++- .../application/sidebar/space/space_bloc.dart | 12 --- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++------ .../example/macos/Runner/AppDelegate.swift | 6 +- .../lib/appflowy_backend.dart | 17 +--- .../packages/appflowy_backend/lib/log.dart | 79 ++++++++----------- .../packages/appflowy_backend/pubspec.yaml | 2 +- .../packages/appflowy_result/pubspec.yaml | 39 +-------- .../packages/flowy_infra/pubspec.yaml | 41 +--------- .../pubspec.yaml | 4 +- .../flowy_infra_ui_web/pubspec.yaml | 4 +- frontend/appflowy_flutter/pubspec.lock | 52 ++++++++---- frontend/appflowy_flutter/pubspec.yaml | 10 ++- 17 files changed, 170 insertions(+), 221 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 522b694edb..a7fcccd186 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; @@ -176,12 +175,7 @@ class _CalloutBlockComponentWidgetState EmojiIconData result = EmojiIconData.emoji('📌'); try { result = EmojiIconData(FlowyIconType.values.byName(type), icon); - } catch (e) { - Log.info( - 'get emoji error with icon:[$icon], type:[$type] within calloutBlockComponentWidget', - e, - ); - } + } catch (_) {} return result; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart index 0987bc29d3..1a2e21c305 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart @@ -238,8 +238,13 @@ extension TableNodeExtension on Node { try { final columnWidths = parentTableNode.attributes[SimpleTableBlockKeys.columnWidths]; - final width = columnWidths?[columnIndex.toString()]; - return width ?? SimpleTableConstants.defaultColumnWidth; + final width = columnWidths?[columnIndex.toString()] as Object?; + if (width == null) { + return SimpleTableConstants.defaultColumnWidth; + } + return width.toDouble( + defaultValue: SimpleTableConstants.defaultColumnWidth, + ); } catch (e) { Log.warn('get column width: $e'); return SimpleTableConstants.defaultColumnWidth; @@ -856,3 +861,18 @@ extension TableNodeExtension on Node { return TableAlign.left; } } + +extension on Object { + double toDouble({double defaultValue = 0}) { + if (this is double) { + return this as double; + } + if (this is String) { + return double.tryParse(this as String) ?? defaultValue; + } + if (this is int) { + return (this as int).toDouble(); + } + return defaultValue; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 7fa18fc54d..2e9b2c9afd 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -119,7 +119,7 @@ class FlowyRunner { // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. MemoryLeakDetectorTask(), - const DebugTask(), + DebugTask(), const FeatureFlagTask(), // localization diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index a398db3061..2be124e2fe 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -17,7 +17,6 @@ import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_b import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -64,7 +63,6 @@ class InitAppWidgetTask extends LaunchTask { child: widget, ); - Bloc.observer = ApplicationBlocObserver(); runApp( EasyLocalization( supportedLocales: const [ @@ -283,14 +281,6 @@ class AppGlobals { static BuildContext get context => rootNavKey.currentContext!; } -class ApplicationBlocObserver extends BlocObserver { - @override - void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - Log.debug(error); - super.onError(bloc, error, stackTrace); - } -} - Future appTheme(String themeName) async { if (themeName.isEmpty) { return AppTheme.fallback; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart index 082e25e250..9a34e84f70 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart @@ -1,18 +1,45 @@ +import 'package:appflowy/startup/startup.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:talker/talker.dart'; +import 'package:talker_bloc_logger/talker_bloc_logger.dart'; import 'package:universal_platform/universal_platform.dart'; -import '../startup.dart'; - class DebugTask extends LaunchTask { - const DebugTask(); + DebugTask(); + + final Talker talker = Talker(); @override Future initialize(LaunchContext context) async { - // the hotkey manager is not supported on mobile + // hide the keyboard on mobile if (UniversalPlatform.isMobile && kDebugMode) { await SystemChannels.textInput.invokeMethod('TextInput.hide'); } + + // log the bloc events + if (kDebugMode) { + Bloc.observer = TalkerBlocObserver( + talker: talker, + settings: TalkerBlocLoggerSettings( + // Disabled by default to prevent mixing with AppFlowy logs + // Enable to observe all bloc events + enabled: false, + printEventFullData: false, + printStateFullData: false, + printChanges: true, + printClosings: true, + printCreations: true, + transitionFilter: (_, transition) { + // By default, observe all transitions + // You can add your own filter here if needed + // when you want to observer a specific bloc + return true; + }, + ), + ); + } } @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index 0f1dc4e987..d4c1b4fe16 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -331,16 +331,6 @@ class SpaceBloc extends Bloc { final (spaces, _, _) = await _getSpaces(); final currentSpace = await _getLastOpenedSpace(spaces); - Log.info( - 'receive space update, current space: ${currentSpace?.name}(${currentSpace?.id})', - ); - - for (var i = 0; i < spaces.length; i++) { - Log.info( - 'receive space update[$i]: ${spaces[i].name}(${spaces[i].id})', - ); - } - emit( state.copyWith( spaces: spaces, @@ -496,7 +486,6 @@ class SpaceBloc extends Bloc { } void _initial(UserProfilePB userProfile, String workspaceId) { - Log.info('initial(or reset) space bloc: $workspaceId, ${userProfile.id}'); _workspaceService = WorkspaceService(workspaceId: workspaceId); this.userProfile = userProfile; @@ -507,7 +496,6 @@ class SpaceBloc extends Bloc { workspaceId: workspaceId, )..start( sectionChanged: (result) async { - Log.info('did receive section views changed'); if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift index d53ef64377..b3c1761412 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index 69e287f117..f69fd16927 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -4,11 +4,11 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:logger/logger.dart'; import 'ffi.dart' as ffi; @@ -62,28 +62,15 @@ class RustLogStreamReceiver { late StreamController _streamController; late StreamSubscription _subscription; int get port => _ffiPort.sendPort.nativePort; - late Logger _logger; RustLogStreamReceiver._internal() { _ffiPort = RawReceivePort(); _streamController = StreamController(); _ffiPort.handler = _streamController.add; - _logger = Logger( - printer: PrettyPrinter( - methodCount: 0, // number of method calls to be displayed - errorMethodCount: 8, // number of method calls if stacktrace is provided - lineLength: 120, // width of the output - colors: false, // Colorful log messages - printEmojis: false, // Print an emoji for each log message - dateTimeFormat: - DateTimeFormat.none, // Should each log print contain a timestamp - ), - level: kDebugMode ? Level.trace : Level.info, - ); _subscription = _streamController.stream.listen((data) { String decodedString = utf8.decode(data); - _logger.i(decodedString); + Log.info(decodedString); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart index 355a196621..ce0a4e2248 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart @@ -3,64 +3,43 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; -import 'package:logger/logger.dart'; +import 'package:talker/talker.dart'; import 'ffi.dart'; class Log { static final shared = Log(); - // ignore: unused_field - late Logger _logger; - bool _enabled = false; + late Talker _logger; + + bool enableFlutterLog = true; // used to disable log in tests @visibleForTesting bool disableLog = false; Log() { - _logger = Logger( - printer: PrettyPrinter( - methodCount: 2, // Number of method calls to be displayed - errorMethodCount: 8, // Number of method calls if stacktrace is provided - lineLength: 120, // Width of the output - colors: true, // Colorful log messages - printEmojis: true, // Print an emoji for each log message - ), - level: kDebugMode ? Level.trace : Level.info, + _logger = Talker( + filter: LogLevelTalkerFilter(), ); } - static void enableFlutterLog() { - shared._enabled = true; - } - // Generic internal logging function to reduce code duplication - static void _log(Level level, int rustLevel, dynamic msg, - [dynamic error, StackTrace? stackTrace]) { - if (shared._enabled) { - switch (level) { - case Level.info: - shared._logger.i(msg, stackTrace: stackTrace); - break; - case Level.debug: - shared._logger.d(msg, stackTrace: stackTrace); - break; - case Level.warning: - shared._logger.w(msg, stackTrace: stackTrace); - break; - case Level.error: - shared._logger.e(msg, stackTrace: stackTrace); - break; - case Level.trace: - shared._logger.t(msg, stackTrace: stackTrace); - break; - default: - shared._logger.log(level, msg, stackTrace: stackTrace); - } + static void _log( + LogLevel level, + int rustLevel, + dynamic msg, [ + dynamic error, + StackTrace? stackTrace, + ]) { + // only forward logs to flutter in debug mode, otherwise log to rust to + // persist logs in the file system + if (shared.enableFlutterLog && kDebugMode) { + shared._logger.log(msg, logLevel: level, stackTrace: stackTrace); + } else { + String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace); + rust_log(rustLevel, toNativeUtf8(formattedMessage)); } - String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace); - rust_log(rustLevel, toNativeUtf8(formattedMessage)); } static void info(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -68,7 +47,7 @@ class Log { return; } - _log(Level.info, 0, msg, error, stackTrace); + _log(LogLevel.info, 0, msg, error, stackTrace); } static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -76,7 +55,7 @@ class Log { return; } - _log(Level.debug, 1, msg, error, stackTrace); + _log(LogLevel.debug, 1, msg, error, stackTrace); } static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -84,7 +63,7 @@ class Log { return; } - _log(Level.warning, 3, msg, error, stackTrace); + _log(LogLevel.warning, 3, msg, error, stackTrace); } static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -92,7 +71,7 @@ class Log { return; } - _log(Level.trace, 2, msg, error, stackTrace); + _log(LogLevel.verbose, 2, msg, error, stackTrace); } static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -100,7 +79,7 @@ class Log { return; } - _log(Level.error, 4, msg, error, stackTrace); + _log(LogLevel.error, 4, msg, error, stackTrace); } } @@ -119,3 +98,11 @@ String _formatMessageWithStackTrace(dynamic msg, StackTrace? stackTrace) { } return msg.toString(); } + +class LogLevelTalkerFilter implements TalkerFilter { + @override + bool filter(TalkerData data) { + // filter out the debug logs in release mode + return kDebugMode ? true : data.logLevel != LogLevel.debug; + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml index 9ff267929a..18aea4838b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: ffi: ^2.0.2 isolates: ^3.0.3+8 protobuf: ^3.1.0 - logger: ^2.4.0 + talker: ^4.7.1 plugin_platform_interface: ^2.1.3 appflowy_result: path: ../appflowy_result diff --git a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml index fa2e35f329..5d8f0d88c2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml @@ -1,7 +1,7 @@ name: appflowy_result description: "A new Flutter package project." version: 0.0.1 -homepage: +homepage: https://github.com/appflowy-io/appflowy environment: sdk: ">=3.3.0 <4.0.0" @@ -9,40 +9,3 @@ environment: dev_dependencies: flutter_lints: ^3.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index cb5cbb9cee..478c649664 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -1,5 +1,5 @@ name: flowy_infra -description: A new Flutter package project. +description: AppFlowy Infra. version: 0.0.1 homepage: https://appflowy.io @@ -15,7 +15,7 @@ dependencies: path: ^1.8.2 time: ">=2.0.0" uuid: ">=2.2.2" - bloc: ^8.1.2 + bloc: ^9.0.0 freezed_annotation: ^2.1.0 file_picker: ^8.0.2 file: ^7.0.0 @@ -25,40 +25,3 @@ dev_dependencies: flutter_lints: ^3.0.1 freezed: ^2.4.7 json_serializable: ^6.5.4 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml index f2e3eb8749..4a8ad910cb 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_platform_interface description: A new Flutter package project. version: 0.0.1 -homepage: +homepage: https://github.com/appflowy-io/appflowy environment: sdk: ">=2.12.0 <3.0.0" @@ -17,5 +17,3 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 - -flutter: \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml index bbdac0d2e4..d4364a6400 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_web description: A new Flutter package project. version: 0.0.1 -homepage: +homepage: https://github.com/appflowy-io/appflowy publish_to: none environment: @@ -25,4 +25,4 @@ flutter: platforms: web: pluginClass: FlowyInfraUIPlugin - fileName: flowy_infra_ui_web.dart \ No newline at end of file + fileName: flowy_infra_ui_web.dart diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index ea4cb90fe1..e468ba89b4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -30,6 +30,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.11" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" any_date: dependency: "direct main" description: @@ -253,18 +261,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" url: "https://pub.dev" source: hosted - version: "9.1.7" + version: "10.0.0" boolean_selector: dependency: transitive description: @@ -783,10 +791,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" url: "https://pub.dev" source: hosted - version: "8.1.6" + version: "9.1.0" flutter_cache_manager: dependency: "direct main" description: @@ -1331,14 +1339,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.6" - logger: - dependency: transitive - description: - name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 - url: "https://pub.dev" - source: hosted - version: "2.5.0" logging: dependency: transitive description: @@ -2186,6 +2186,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + talker: + dependency: "direct main" + description: + name: talker + sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" + url: "https://pub.dev" + source: hosted + version: "4.7.1" + talker_bloc_logger: + dependency: "direct main" + description: + name: talker_bloc_logger + sha256: "2214a5f6ef9ff33494dc6149321c270356962725cc8fc1a485d44b1d9b812ddd" + url: "https://pub.dev" + source: hosted + version: "4.7.1" + talker_logger: + dependency: transitive + description: + name: talker_logger + sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 + url: "https://pub.dev" + source: hosted + version: "4.7.1" term_glyph: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 77663760a2..48d380a622 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 - bloc: ^8.1.2 + bloc: ^9.0.0 cached_network_image: ^3.3.0 calendar_view: git: @@ -67,7 +67,7 @@ dependencies: flutter: sdk: flutter flutter_animate: ^4.5.0 - flutter_bloc: ^8.1.3 + flutter_bloc: ^9.1.0 flutter_cache_manager: ^3.3.1 flutter_chat_core: 0.0.2 flutter_chat_ui: ^2.0.0-dev.1 @@ -148,9 +148,13 @@ dependencies: xml: ^6.5.0 window_manager: ^0.4.3 saver_gallery: ^4.0.1 + talker_bloc_logger: ^4.7.1 + talker: ^4.7.1 dev_dependencies: - bloc_test: ^9.1.2 + # Introduce talker to log the bloc events, and only log the events in the development mode + + bloc_test: ^10.0.0 build_runner: ^2.4.9 envied_generator: ^1.0.1 flutter_lints: ^5.0.0 From ccb020e8851759d823cd77129c60aedf32372a12 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 14:16:58 +0800 Subject: [PATCH 228/384] chore: bump lai --- frontend/rust-lib/Cargo.lock | 125 ++++++++++-------- frontend/rust-lib/Cargo.toml | 5 +- frontend/rust-lib/flowy-ai/Cargo.toml | 7 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 2 +- frontend/rust-lib/flowy-ai/src/entities.rs | 2 +- frontend/rust-lib/flowy-ai/src/lib.rs | 5 +- .../flowy-ai/src/local_ai/controller.rs | 6 +- .../flowy-ai/src/local_ai/resource.rs | 2 +- .../flowy-ai/src/local_ai/stream_util.rs | 2 +- frontend/rust-lib/flowy-ai/src/mcp/client.rs | 45 ------- frontend/rust-lib/flowy-ai/src/mcp/manager.rs | 50 ++----- frontend/rust-lib/flowy-ai/src/mcp/mod.rs | 2 - frontend/rust-lib/flowy-ai/src/mcp/util.rs | 6 - .../src/middleware/chat_service_mw.rs | 2 +- frontend/rust-lib/flowy-core/Cargo.toml | 2 +- .../src/deps_resolve/database_deps.rs | 2 +- .../rust-lib/flowy-core/src/log_filter.rs | 4 +- frontend/scripts/tool/update_local_ai_rev.sh | 2 +- 18 files changed, 103 insertions(+), 168 deletions(-) delete mode 100644 frontend/rust-lib/flowy-ai/src/mcp/client.rs delete mode 100644 frontend/rust-lib/flowy-ai/src/mcp/util.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 7b4967728f..07b39ecaa0 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -342,6 +342,61 @@ dependencies = [ "subtle", ] +[[package]] +name = "af-local-ai" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +dependencies = [ + "af-plugin", + "anyhow", + "bytes", + "futures", + "reqwest 0.11.27", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "zip 2.2.0", + "zip-extensions", +] + +[[package]] +name = "af-mcp" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +dependencies = [ + "anyhow", + "futures-util", + "mcp_daemon", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "af-plugin" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +dependencies = [ + "anyhow", + "cfg-if", + "crossbeam-utils", + "log", + "once_cell", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror 1.0.64", + "tokio", + "tokio-stream", + "tracing", + "winreg 0.55.0", + "xattr", +] + [[package]] name = "again" version = "0.1.2" @@ -434,9 +489,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" @@ -473,47 +528,6 @@ dependencies = [ "thiserror 1.0.64", ] -[[package]] -name = "appflowy-local-ai" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=52ad76f21f8f3a7b510dae029836b5fe86479e5a#52ad76f21f8f3a7b510dae029836b5fe86479e5a" -dependencies = [ - "anyhow", - "appflowy-plugin", - "bytes", - "futures", - "reqwest 0.11.27", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "zip 2.2.0", - "zip-extensions", -] - -[[package]] -name = "appflowy-plugin" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=52ad76f21f8f3a7b510dae029836b5fe86479e5a#52ad76f21f8f3a7b510dae029836b5fe86479e5a" -dependencies = [ - "anyhow", - "cfg-if", - "crossbeam-utils", - "log", - "once_cell", - "parking_lot 0.12.1", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tracing", - "winreg 0.55.0", - "xattr", -] - [[package]] name = "arbitrary" version = "1.3.2" @@ -2450,10 +2464,11 @@ dependencies = [ name = "flowy-ai" version = "0.1.0" dependencies = [ + "af-local-ai", + "af-mcp", + "af-plugin", "allo-isolate", "anyhow", - "appflowy-local-ai", - "appflowy-plugin", "arc-swap", "base64 0.21.5", "bytes", @@ -2472,7 +2487,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "log", - "mcp_daemon", "md5", "notify", "pin-project", @@ -2545,8 +2559,8 @@ dependencies = [ name = "flowy-core" version = "0.1.0" dependencies = [ + "af-local-ai", "anyhow", - "appflowy-local-ai", "arc-swap", "base64 0.21.5", "bytes", @@ -6664,9 +6678,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -7498,22 +7512,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", "libc", - "mio 0.8.9", - "num_cpus", + "mio 1.0.3", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -7528,9 +7541,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 843a939ae3..02df770eb1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,5 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "52ad76f21f8f3a7b510dae029836b5fe86479e5a" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "52ad76f21f8f3a7b510dae029836b5fe86479e5a" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 2fe8b724c9..22cecd3713 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -12,6 +12,7 @@ flowy-error = { path = "../flowy-error", features = [ "impl_from_dispatch_error", "impl_from_collab_folder", "impl_from_sqlite", + "impl_from_appflowy_cloud", ] } lib-dispatch = { workspace = true } tracing.workspace = true @@ -34,8 +35,8 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } -appflowy-plugin = { version = "0.1.0" } +af-local-ai = { version = "0.1.0", features = ["verbose"] } +af-plugin = { version = "0.1.0" } reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" base64 = "0.21.5" @@ -50,7 +51,7 @@ collab-integrate.workspace = true [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] notify = "6.1.1" -mcp_daemon = "0.2.1" +af-mcp = { version = "0.1.0" } [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.55" diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 03a18dcdec..cb9030ea18 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -8,7 +8,7 @@ use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; -use appflowy_plugin::manager::PluginManager; +use af_plugin::manager::PluginManager; use dashmap::DashMap; use flowy_ai_pub::cloud::{AIModel, ChatCloudService, ChatSettings, UpdateChatParams}; use flowy_error::{FlowyError, FlowyResult}; diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 0e996228c6..fe65a1cc4c 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -1,4 +1,4 @@ -use appflowy_plugin::core::plugin::RunningState; +use af_plugin::core::plugin::RunningState; use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 046a1ea6b9..3a15eec576 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -6,7 +6,10 @@ mod chat; mod completion; pub mod entities; mod local_ai; -mod mcp; + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub mod mcp; + mod middleware; pub mod notification; mod persistence; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 503345e206..59113a59ae 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -4,8 +4,8 @@ use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; +use af_plugin::manager::PluginManager; use anyhow::Error; -use appflowy_plugin::manager::PluginManager; use flowy_ai_pub::cloud::{ChatCloudService, ChatMessageMetadata, ContextLoader, LocalAIConfig}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; @@ -15,8 +15,8 @@ use std::collections::HashMap; use crate::local_ai::watch::is_plugin_ready; use crate::stream_message::StreamMessage; -use appflowy_local_ai::ollama_plugin::OllamaAIPlugin; -use appflowy_plugin::core::plugin::RunningState; +use af_local_ai::ollama_plugin::OllamaAIPlugin; +use af_plugin::core::plugin::RunningState; use arc_swap::ArcSwapOption; use futures_util::SinkExt; use lib_infra::util::get_operating_system; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index a162096f7d..3eddcf2039 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -11,7 +11,7 @@ use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; -use appflowy_local_ai::ollama_plugin::OllamaPluginConfig; +use af_local_ai::ollama_plugin::OllamaPluginConfig; use lib_infra::util::{get_operating_system, OperatingSystem}; use reqwest::Client; use serde::Deserialize; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs b/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs index 76eb01ea6f..fbe4157c8c 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs @@ -1,4 +1,4 @@ -use appflowy_plugin::error::PluginError; +use af_plugin::error::PluginError; use flowy_ai_pub::cloud::QuestionStreamValue; use flowy_error::FlowyError; diff --git a/frontend/rust-lib/flowy-ai/src/mcp/client.rs b/frontend/rust-lib/flowy-ai/src/mcp/client.rs deleted file mode 100644 index 8f03e75c31..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/client.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::mcp::util::map_mcp_error; -use flowy_error::{FlowyError, FlowyResult}; -use mcp_daemon::transport::{ClientStdioTransport, Transport}; -use mcp_daemon::types::Implementation; -use mcp_daemon::Client; - -pub struct MCPServerConfig { - server_cmd: String, - args: Vec, -} - -impl MCPServerConfig { - pub fn is_sse_server(&self) -> bool { - self.server_cmd.starts_with("http") - } -} - -// https://modelcontextprotocol.io/docs/concepts/tools -#[derive(Clone)] -pub struct MCPClient { - pub client: Client, - pub transport: ClientStdioTransport, -} - -impl MCPClient { - pub async fn initialize(&self) -> Result<(), FlowyError> { - self.transport.open().await.map_err(map_mcp_error)?; - self.client.start().await.map_err(map_mcp_error)?; - - let implementation = Implementation { - name: "test".to_string(), - version: "0.0.1".to_string(), - }; - self - .client - .initialize(implementation) - .map(|err| map_mcp_error)?; - Ok(()) - } - - pub async fn stop(&mut self) -> FlowyResult<()> { - self.transport.close().await.map_err(map_mcp_error)?; - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs b/frontend/rust-lib/flowy-ai/src/mcp/manager.rs index 18450f266d..9e40a51f68 100644 --- a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs +++ b/frontend/rust-lib/flowy-ai/src/mcp/manager.rs @@ -1,29 +1,8 @@ -use crate::mcp::client::MCPClient; -use anyhow::Context; +use af_mcp::client::{MCPClient, MCPServerConfig}; +use af_mcp::entities::ToolsList; use dashmap::DashMap; -use flowy_error::{ErrorCode, FlowyError}; -use mcp_daemon::transport::{ - ClientHttpTransport, ClientStdioTransport, ServerStdioTransport, Transport, TransportError, -}; -use mcp_daemon::types::Implementation; -use mcp_daemon::Client; -use serde_json::Value; -use std::io::{BufRead, BufReader}; -use std::process::{Child, Command, Stdio}; +use flowy_error::FlowyError; use std::sync::Arc; -use std::thread; -use tracing::{debug, info}; - -pub struct MCPServerConfig { - server_cmd: String, - args: Vec, -} - -impl MCPServerConfig { - pub fn is_sse_server(&self) -> bool { - self.server_cmd.starts_with("http") - } -} pub struct MCPClientManager { stdio_clients: Arc>, @@ -37,7 +16,7 @@ impl MCPClientManager { } pub async fn connect_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { - let client = connect_to_stdio_server(&config.server_cmd, config.args.as_ref()).await?; + let client = MCPClient::new_stdio(config.clone()).await?; self.stdio_clients.insert(config.server_cmd, client.clone()); client.initialize().await?; Ok(()) @@ -50,20 +29,11 @@ impl MCPClientManager { } Ok(()) } -} -async fn connect_to_stdio_server(command: &str, args: &[&str]) -> Result { - info!( - "Connecting to running server with command: {} {}", - command, - args.join(" ") - ); - - let transport = ClientStdioTransport::new(command, args).map_err(map_mcp_error)?; - let client = Client::builder(transport.clone()).build(); - Ok(MCPClient { client, transport }) -} - -fn map_mcp_error(err: TransportError) -> FlowyError { - FlowyError::new(ErrorCode::MCPError, err.to_string()) + pub async fn tool_list(&self, server_cmd: &str) -> Option { + let client = self.stdio_clients.get(server_cmd)?; + let tools = client.list_tools().await.ok(); + tracing::trace!("{}: tool list: {:?}", server_cmd, tools); + tools + } } diff --git a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs index 1286a7cf46..8f73c8326c 100644 --- a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs +++ b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs @@ -1,3 +1 @@ -mod client; mod manager; -mod util; diff --git a/frontend/rust-lib/flowy-ai/src/mcp/util.rs b/frontend/rust-lib/flowy-ai/src/mcp/util.rs deleted file mode 100644 index 1288e9baa6..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/util.rs +++ /dev/null @@ -1,6 +0,0 @@ -use flowy_error::{ErrorCode, FlowyError}; -use mcp_daemon::transport::TransportError; - -pub(crate) fn map_mcp_error(err: TransportError) -> FlowyError { - FlowyError::new(ErrorCode::MCPError, err.to_string()) -} diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 37bf5b5daf..477249a944 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -5,7 +5,7 @@ use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use crate::persistence::{select_single_message, ChatMessageTable}; -use appflowy_plugin::error::PluginError; +use af_plugin::error::PluginError; use std::collections::HashMap; use flowy_ai_pub::cloud::{ diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 8c33996046..f535dd757c 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -38,7 +38,7 @@ flowy-storage-pub = { workspace = true } client-api.workspace = true flowy-ai = { workspace = true } flowy-ai-pub = { workspace = true } -appflowy-local-ai = { version = "0.1.0" } +af-local-ai = { version = "0.1.0" } tracing.workspace = true diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 328b90aa15..715b8207b8 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -1,4 +1,4 @@ -use appflowy_local_ai::ai_ops::{LocalAITranslateItem, LocalAITranslateRowData}; +use af_local_ai::ai_ops::{LocalAITranslateItem, LocalAITranslateRowData}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_ai::ai_manager::AIManager; diff --git a/frontend/rust-lib/flowy-core/src/log_filter.rs b/frontend/rust-lib/flowy-core/src/log_filter.rs index 63877862f0..6704ad0507 100644 --- a/frontend/rust-lib/flowy-core/src/log_filter.rs +++ b/frontend/rust-lib/flowy-core/src/log_filter.rs @@ -57,8 +57,8 @@ pub fn create_log_filter( filters.push(format!("lib_infra={}", level)); filters.push(format!("flowy_search={}", level)); filters.push(format!("flowy_chat={}", level)); - filters.push(format!("appflowy_local_ai={}", level)); - filters.push(format!("appflowy_plugin={}", level)); + filters.push(format!("af_local_ai={}", level)); + filters.push(format!("af_plugin={}", level)); filters.push(format!("flowy_ai={}", level)); filters.push(format!("flowy_storage={}", level)); // Enable the frontend logs. DO NOT DISABLE. diff --git a/frontend/scripts/tool/update_local_ai_rev.sh b/frontend/scripts/tool/update_local_ai_rev.sh index 83c5e67d80..af24e0ba9f 100755 --- a/frontend/scripts/tool/update_local_ai_rev.sh +++ b/frontend/scripts/tool/update_local_ai_rev.sh @@ -15,7 +15,7 @@ for dir in "${directories[@]}"; do pushd "$dir" > /dev/null # Define the crates to update - crates=("appflowy-local-ai" "appflowy-plugin") + crates=("af-local-ai" "af-plugin" "af-mcp") for crate in "${crates[@]}"; do sed -i.bak "/^${crate}[[:alnum:]-]*[[:space:]]*=/s/rev = \"[a-fA-F0-9]\{6,40\}\"/rev = \"$NEW_REV\"/g" Cargo.toml From a26ebbccc1a3ad3391fa45ac730606d782712b70 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 27 Mar 2025 14:19:51 +0800 Subject: [PATCH 229/384] fix: toolbar launch review issues (#7631) * fix: keep the turn into menu within six-dot same as toolbar * fix: change some icon color within toolbar * fix: improve toolbar * chore: update editor dependency * fix: update editor dependency --- .../document/presentation/editor_page.dart | 16 +- .../option/turn_into_option_action.dart | 321 +++++++----------- .../ai/ai_writer_toolbar_item.dart | 9 +- .../base/toolbar_extension.dart | 14 +- .../desktop_floating_toolbar.dart | 4 +- .../link/link_create_menu.dart | 109 ++++++ .../desktop_toolbar/link/link_hover_menu.dart | 75 ++++ .../custom_format_toolbar_items.dart | 3 +- .../custom_link_toolbar_item.dart | 147 +------- .../custom_placeholder_toolbar_item.dart | 4 +- .../custom_text_align_toolbar_item.dart | 31 +- .../more_option_toolbar_item.dart | 131 ++++++- .../text_heading_toolbar_item.dart | 7 +- .../text_suggestions_toolbar_item.dart | 142 ++++++-- .../appflowy_flutter/lib/startup/startup.dart | 2 + .../appearance/desktop_appearance.dart | 11 +- .../flowy_infra/lib/theme_extension_v2.dart | 91 +++++ frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 19 files changed, 714 insertions(+), 409 deletions(-) create mode 100644 frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index d0d26c43e5..a6cff2ae07 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -22,6 +22,7 @@ import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -350,6 +351,8 @@ class _AppFlowyEditorPageState extends State final isViewDeleted = context.read().state.isDeleted; final isLocked = context.read()?.state.isLocked ?? false; + + final themeV2 = AFThemeExtensionV2.of(context); final editor = Directionality( textDirection: textDirection, child: AppFlowyEditor( @@ -428,7 +431,6 @@ class _AppFlowyEditorPageState extends State ), ); } - return Center( child: FloatingToolbar( floatingToolbarHeight: 40, @@ -436,12 +438,18 @@ class _AppFlowyEditorPageState extends State style: FloatingToolbarStyle( backgroundColor: Theme.of(context).cardColor, toolbarActiveColor: Color(0xffe0f8fd), - toolbarElevation: 10, ), items: toolbarItems, - decoration: ShapeDecoration( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), color: Theme.of(context).cardColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + boxShadow: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 24, + color: themeV2.shadow_medium, + ), + ], ), toolbarBuilder: (context, child, onDismiss, isMetricsChanged) => DesktopFloatingToolbar( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart index ffb238303e..c927fcf85f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart @@ -2,8 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys, quoteNode; @@ -149,214 +148,134 @@ class TurnIntoOptionMenu extends StatelessWidget { @override Widget build(BuildContext context) { + if (hasNonSupportedTypes) { + return buildItem( + pateItem, + textSuggestionItem, + context.read().editorState, + ); + } + + return _buildTurnIntoOptions(context, node); + } + + Widget _buildTurnIntoOptions(BuildContext context, Node node) { + final editorState = context.read().editorState; + SuggestionItem currentSuggestionItem = textSuggestionItem; + final List suggestionItems = suggestions.sublist(0, 4); + final List turnIntoItems = + suggestions.sublist(4, suggestions.length); + final textColor = Color(0xff99A1A8); + + void refreshSuggestions() { + final selection = editorState.selection; + if (selection == null || !selection.isSingle) return; + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null || node.delta == null) return; + final nodeType = node.type; + SuggestionType? suggestionType; + if (nodeType == HeadingBlockKeys.type) { + final level = node.attributes[HeadingBlockKeys.level] ?? 1; + if (level == 1) { + suggestionType = SuggestionType.h1; + } else if (level == 2) { + suggestionType = SuggestionType.h2; + } else if (level == 3) { + suggestionType = SuggestionType.h3; + } + } else if (nodeType == ToggleListBlockKeys.type) { + final level = node.attributes[ToggleListBlockKeys.level]; + if (level == null) { + suggestionType = SuggestionType.toggle; + } else if (level == 1) { + suggestionType = SuggestionType.toggleH1; + } else if (level == 2) { + suggestionType = SuggestionType.toggleH2; + } else if (level == 3) { + suggestionType = SuggestionType.toggleH3; + } + } else { + suggestionType = nodeType2SuggestionType[nodeType]; + } + if (suggestionType == null) return; + suggestionItems.clear(); + turnIntoItems.clear(); + for (final item in suggestions) { + if (item.type.group == suggestionType.group && + item.type != suggestionType) { + suggestionItems.add(item); + } else { + turnIntoItems.add(item); + } + } + currentSuggestionItem = + suggestions.where((item) => item.type == suggestionType).first; + } + + refreshSuggestions(); + return Column( mainAxisSize: MainAxisSize.min, - children: _buildTurnIntoOptions(context, node), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildSubTitle( + LocaleKeys.document_toolbar_suggestions.tr(), + textColor, + ), + ...List.generate(suggestionItems.length, (index) { + return buildItem( + suggestionItems[index], + currentSuggestionItem, + editorState, + ); + }), + buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor), + ...List.generate(turnIntoItems.length, (index) { + return buildItem( + turnIntoItems[index], + currentSuggestionItem, + editorState, + ); + }), + ], ); } - List _buildTurnIntoOptions(BuildContext context, Node node) { - final children = []; - - if (hasNonSupportedTypes) { - return children - ..add( - _TurnInfoButton( - type: SubPageBlockKeys.type, - node: node, - ), - ); - } - - for (final type in EditorOptionActionType.turnInto.supportTypes) { - if (type == ToggleListBlockKeys.type) { - // toggle list block and toggle heading block are the same type, - // but they have different attributes. - - // toggle list block - children.add( - _TurnInfoButton( - type: type, - node: node, - ), - ); - - // toggle heading block - for (final i in [1, 2, 3]) { - children.add( - _TurnInfoButton( - type: type, - node: node, - level: i, - ), - ); - } - } else if (type != HeadingBlockKeys.type) { - children.add( - _TurnInfoButton( - type: type, - node: node, - ), - ); - } else { - for (final i in [1, 2, 3]) { - children.add( - _TurnInfoButton( - type: type, - node: node, - level: i, - ), - ); - } - } - } - - return children; - } -} - -class _TurnInfoButton extends StatelessWidget { - const _TurnInfoButton({ - required this.type, - required this.node, - this.level, - }); - - final String type; - final Node node; - final int? level; - - @override - Widget build(BuildContext context) { - final name = _buildLocalization(type, level: level); - final leftIcon = _buildLeftIcon(type, level: level); - final rightIcon = _buildRightIcon(type, node, level: level); - - return HoverButton( - name: name, - leftIcon: FlowySvg(leftIcon), - rightIcon: rightIcon, - itemHeight: ActionListSizes.itemHeight, - onTap: () => BlockActionOptionCubit.turnIntoBlock( - type, - node, - context.read().editorState, - level: level, - currentViewId: getIt().latestOpenView?.id, + Widget buildSubTitle(String text, Color color) { + return Container( + height: 32, + margin: EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.semibold( + text, + color: color, + figmaLineHeight: 16, + ), ), ); } - Widget? _buildRightIcon(String type, Node node, {int? level}) { - if (type != node.type) { - return null; - } - - if (node.type == HeadingBlockKeys.type) { - final nodeLevel = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level != nodeLevel) { - return null; - } - } - - if (node.type == ToggleListBlockKeys.type) { - final nodeLevel = node.attributes[ToggleListBlockKeys.level]; - if (level != nodeLevel) { - return null; - } - } - - return const FlowySvg( - FlowySvgs.workspace_selected_s, - blendMode: null, + Widget buildItem( + SuggestionItem item, + SuggestionItem currentSuggestionItem, + EditorState state, + ) { + final isSelected = item.type == currentSuggestionItem.type; + return SizedBox( + height: 36, + child: FlowyButton( + leftIconSize: const Size.square(20), + leftIcon: FlowySvg(item.svg), + iconPadding: 12, + text: FlowyText( + item.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null, + onTap: () => item.onTap.call(state, false), + ), ); } - - FlowySvgData _buildLeftIcon(String type, {int? level}) { - if (type == ParagraphBlockKeys.type) { - return FlowySvgs.type_text_m; - } else if (type == HeadingBlockKeys.type) { - switch (level) { - case 1: - return FlowySvgs.type_h1_m; - case 2: - return FlowySvgs.type_h2_m; - case 3: - return FlowySvgs.type_h3_m; - default: - return FlowySvgs.type_text_m; - } - } else if (type == QuoteBlockKeys.type) { - return FlowySvgs.type_quote_m; - } else if (type == BulletedListBlockKeys.type) { - return FlowySvgs.type_bulleted_list_m; - } else if (type == NumberedListBlockKeys.type) { - return FlowySvgs.type_numbered_list_m; - } else if (type == TodoListBlockKeys.type) { - return FlowySvgs.type_todo_m; - } else if (type == CalloutBlockKeys.type) { - return FlowySvgs.type_callout_m; - } else if (type == SubPageBlockKeys.type) { - return FlowySvgs.icon_document_s; - } else if (type == ToggleListBlockKeys.type) { - switch (level) { - case 1: - return FlowySvgs.type_toggle_h1_m; - case 2: - return FlowySvgs.type_toggle_h2_m; - case 3: - return FlowySvgs.type_toggle_h3_m; - default: - return FlowySvgs.type_toggle_list_m; - } - } - - throw UnimplementedError('Unsupported block type: $type'); - } - - String _buildLocalization( - String type, { - int? level, - }) { - switch (type) { - case ParagraphBlockKeys.type: - return LocaleKeys.document_slashMenu_name_text.tr(); - case HeadingBlockKeys.type: - switch (level) { - case 1: - return LocaleKeys.document_slashMenu_name_heading1.tr(); - case 2: - return LocaleKeys.document_slashMenu_name_heading2.tr(); - case 3: - return LocaleKeys.document_slashMenu_name_heading3.tr(); - default: - return LocaleKeys.document_slashMenu_name_text.tr(); - } - case QuoteBlockKeys.type: - return LocaleKeys.document_slashMenu_name_quote.tr(); - case BulletedListBlockKeys.type: - return LocaleKeys.editor_bulletedListShortForm.tr(); - case NumberedListBlockKeys.type: - return LocaleKeys.editor_numberedListShortForm.tr(); - case TodoListBlockKeys.type: - return LocaleKeys.editor_checkbox.tr(); - case CalloutBlockKeys.type: - return LocaleKeys.document_slashMenu_name_callout.tr(); - case SubPageBlockKeys.type: - return LocaleKeys.editor_page.tr(); - case ToggleListBlockKeys.type: - switch (level) { - case 1: - return LocaleKeys.editor_toggleHeading1ShortForm.tr(); - case 2: - return LocaleKeys.editor_toggleHeading2ShortForm.tr(); - case 3: - return LocaleKeys.editor_toggleHeading3ShortForm.tr(); - default: - return LocaleKeys.editor_toggleListShortForm.tr(); - } - } - - throw UnimplementedError('Unsupported block type: $type'); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index dc20f2363f..25502fd975 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -118,7 +119,7 @@ class _AiWriterToolbarActionListState extends State { } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final themeV2 = AFThemeExtensionV2.of(context); final child = FlowyIconButton( width: 48, height: 32, @@ -130,13 +131,13 @@ class _AiWriterToolbarActionListState extends State { FlowySvg( FlowySvgs.toolbar_ai_writer_m, size: Size.square(20), - color: iconColor, + color: themeV2.icon_primary, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: iconColor, + color: themeV2.icon_tertiary, ), ], ), @@ -187,7 +188,7 @@ class ImproveWritingButton extends StatelessWidget { icon: FlowySvg( FlowySvgs.toolbar_ai_improve_writing_m, size: Size.square(20.0), - color: Theme.of(context).iconTheme.color, + color: AFThemeExtensionV2.of(context).icon_primary, ), onPressed: () { if (_isAIEnabled(editorState)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart index 73241382dd..77245a9f95 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; @@ -35,9 +37,7 @@ bool onlyShowInSingleTextTypeSelectionAndExcludeTable( notShowInTable(editorState); } -bool enableSuggestions( - EditorState editorState, -) { +bool enableSuggestions(EditorState editorState) { final selection = editorState.selection; if (selection == null || !selection.isSingle) { return false; @@ -46,10 +46,18 @@ bool enableSuggestions( if (node == null) { return false; } + if (isNarrowWindow(editorState)) return false; + return (node.delta != null && suggestionsItemTypes.contains(node.type)) && notShowInTable(editorState); } +bool isNarrowWindow(EditorState editorState) { + final editorSize = editorState.renderBox?.size ?? Size.zero; + if (editorSize.width < 650) return true; + return false; +} + final Set suggestionsItemTypes = { ...toolbarItemWhiteList, ToggleListBlockKeys.type, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart index 076358034c..daa066d50c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -61,10 +62,11 @@ class _DesktopFloatingToolbarState extends State { ) { const toolbarHeight = 40, topLimit = toolbarHeight + 8; final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState); - final menuWidth = isLongMenu ? 650.0 : 420.0; final editorOffset = editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; final editorSize = editorState.renderBox?.size ?? Size.zero; + final menuWidth = + isLongMenu ? (isNarrowWindow(editorState) ? 490.0 : 660.0) : 420.0; final editorRect = editorOffset & editorSize; final left = rect.left, leftStart = 50; final top = diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index 4d3ef71cce..2f5edc195d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -148,6 +148,115 @@ class _LinkCreateMenuState extends State { } } +void showLinkCreateMenu( + BuildContext context, + EditorState editorState, + Selection selection, +) { + final (left, top, right, bottom, alignment) = _getPosition(editorState); + + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + + OverlayEntry? overlay; + + void dismissOverlay() { + keepEditorFocusNotifier.decrease(); + overlay?.remove(); + overlay = null; + } + + keepEditorFocusNotifier.increase(); + overlay = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: left, + right: right, + dismissCallback: () => keepEditorFocusNotifier.decrease(), + builder: (context) { + return LinkCreateMenu( + alignment: alignment, + editorState: editorState, + onSubmitted: (link, isPage) async { + await editorState.formatDelta(selection, { + BuiltInAttributeKey.href: link, + kIsPageLink: isPage, + }); + dismissOverlay(); + }, + onDismiss: dismissOverlay, + ); + }, + ).build(); + + Overlay.of(context, rootOverlay: true).insert(overlay!); +} + +// get a proper position for link menu +( + double? left, + double? top, + double? right, + double? bottom, + LinkMenuAlignment alignment, +) _getPosition( + EditorState editorState, +) { + final rect = editorState.selectionRects().first; + const menuHeight = 222.0, menuWidth = 320.0; + + double? left, right, top, bottom; + LinkMenuAlignment alignment = LinkMenuAlignment.topLeft; + final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero), + editorSize = editorState.renderBox!.size; + final editorBottom = editorSize.height + editorOffset.dy, + editorRight = editorSize.width + editorOffset.dx; + final overflowBottom = rect.bottom + menuHeight > editorBottom, + overflowTop = rect.top - menuHeight < 0, + overflowLeft = rect.left - menuWidth < 0, + overflowRight = rect.right + menuWidth > editorRight; + + if (overflowTop && !overflowBottom) { + /// show at bottom + top = rect.bottom; + } else if (overflowBottom && !overflowTop) { + /// show at top + bottom = editorBottom - rect.top; + } else if (!overflowTop && !overflowBottom) { + /// show at bottom + top = rect.bottom; + } else { + top = 0; + } + + if (overflowLeft && !overflowRight) { + /// show at right + left = rect.left; + } else if (overflowRight && !overflowLeft) { + /// show at left + right = editorRight - rect.right; + } else if (!overflowLeft && !overflowRight) { + /// show at right + left = rect.left; + } else { + left = 0; + } + + if (left != null && top != null) { + alignment = LinkMenuAlignment.bottomRight; + } else if (left != null && bottom != null) { + alignment = LinkMenuAlignment.topRight; + } else if (right != null && top != null) { + alignment = LinkMenuAlignment.bottomLeft; + } else if (right != null && bottom != null) { + alignment = LinkMenuAlignment.topLeft; + } + + return (left, top, right, bottom, alignment); +} + ShapeDecoration buildToolbarLinkDecoration(BuildContext context) => ShapeDecoration( color: Theme.of(context).cardColor, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index e5950b5e79..e47386699b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -57,10 +57,19 @@ class _LinkHoverTriggerState extends State { Attributes get attribute => widget.attribute; + HoverTriggerKey get triggerKey => HoverTriggerKey(widget.node.id, selection); + + @override + void initState() { + super.initState(); + getIt()._add(triggerKey, showLinkHoverMenu); + } + @override void dispose() { hoverMenuController.close(); editMenuController.close(); + getIt()._remove(triggerKey, showLinkHoverMenu); super.dispose(); } @@ -217,6 +226,31 @@ class _LinkHoverTriggerState extends State { .setData(ClipboardServiceData(plainText: href)); hoverMenuController.close(); } + + void removeLink( + EditorState editorState, + Selection selection, + bool isHref, + ) { + if (!isHref) return; + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final index = selection.normalized.startIndex; + final length = selection.length; + final transaction = editorState.transaction + ..formatText( + node, + index, + length, + { + BuiltInAttributeKey.href: null, + kIsPageLink: null, + }, + ); + editorState.apply(transaction); + } } class LinkHoverMenu extends StatelessWidget { @@ -320,3 +354,44 @@ class LinkHoverMenu extends StatelessWidget { ); } } + +class HoverTriggerKey { + HoverTriggerKey(this.nodeId, this.selection); + + final String nodeId; + final Selection selection; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HoverTriggerKey && + runtimeType == other.runtimeType && + nodeId == other.nodeId && + selection == other.selection; + + @override + int get hashCode => nodeId.hashCode ^ selection.hashCode; +} + +class LinkHoverTriggers { + final Map> _map = {}; + + void _add(HoverTriggerKey key, VoidCallback callback) { + final callbacks = _map[key] ?? {}; + callbacks.add(callback); + _map[key] = callbacks; + } + + void _remove(HoverTriggerKey key, VoidCallback callback) { + final callbacks = _map[key] ?? {}; + callbacks.remove(callback); + _map[key] = callbacks; + } + + void call(HoverTriggerKey key) { + final callbacks = _map[key] ?? {}; + for (final callback in callbacks) { + callback.call(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index a171c8ca4d..d665230a59 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -6,6 +6,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; @@ -79,7 +80,7 @@ class _FormatToolbarItem extends ToolbarItem { size: Size.square(20.0), color: (isDark && isHighlight) ? Color(0xFF282E3A) - : Theme.of(context).iconTheme.color, + : AFThemeExtensionV2.of(context).icon_primary, ), onPressed: () => editorState.toggleAttribute( name, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index 355e7b3b7a..fb241d5309 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -1,7 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; @@ -14,7 +17,8 @@ const kIsPageLink = 'is_page_link'; final customLinkItem = ToolbarItem( id: ToolbarId.link.id, group: 4, - isActive: onlyShowInSingleSelectionAndTextType, + isActive: (state) => + !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); @@ -42,9 +46,11 @@ final customLinkItem = ToolbarItem( onPressed: () { toolbarCubit?.dismiss(); if (isHref) { - removeLink(editorState, selection, isHref); + getIt().call( + HoverTriggerKey(nodes.first.id, selection), + ); } else { - _showLinkMenu(context, editorState, selection, isHref); + showLinkCreateMenu(context, editorState, selection); } }, ); @@ -62,78 +68,6 @@ final customLinkItem = ToolbarItem( }, ); -void removeLink( - EditorState editorState, - Selection selection, - bool isHref, -) { - if (!isHref) return; - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..formatText( - node, - index, - length, - { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); -} - -void _showLinkMenu( - BuildContext context, - EditorState editorState, - Selection selection, - bool isHref, -) { - final (left, top, right, bottom, alignment) = _getPosition(editorState); - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - - OverlayEntry? overlay; - - void dismissOverlay() { - keepEditorFocusNotifier.decrease(); - overlay?.remove(); - overlay = null; - } - - keepEditorFocusNotifier.increase(); - overlay = FullScreenOverlayEntry( - top: top, - bottom: bottom, - left: left, - right: right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) { - return LinkCreateMenu( - alignment: alignment, - editorState: editorState, - onSubmitted: (link, isPage) async { - await editorState.formatDelta(selection, { - BuiltInAttributeKey.href: link, - kIsPageLink: isPage, - }); - dismissOverlay(); - }, - onDismiss: dismissOverlay, - ); - }, - ).build(); - - Overlay.of(context, rootOverlay: true).insert(overlay!); -} - extension AttributeExtension on Attributes { bool get isPage { if (this[kIsPageLink] is bool) { @@ -143,69 +77,6 @@ extension AttributeExtension on Attributes { } } -// get a proper position for link menu -( - double? left, - double? top, - double? right, - double? bottom, - LinkMenuAlignment alignment, -) _getPosition( - EditorState editorState, -) { - final rect = editorState.selectionRects().first; - const menuHeight = 222.0, menuWidth = 320.0; - - double? left, right, top, bottom; - LinkMenuAlignment alignment = LinkMenuAlignment.topLeft; - final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero), - editorSize = editorState.renderBox!.size; - final editorBottom = editorSize.height + editorOffset.dy, - editorRight = editorSize.width + editorOffset.dx; - final overflowBottom = rect.bottom + menuHeight > editorBottom, - overflowTop = rect.top - menuHeight < 0, - overflowLeft = rect.left - menuWidth < 0, - overflowRight = rect.right + menuWidth > editorRight; - - if (overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else if (overflowBottom && !overflowTop) { - /// show at top - bottom = editorBottom - rect.top; - } else if (!overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else { - top = 0; - } - - if (overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else if (overflowRight && !overflowLeft) { - /// show at left - right = editorRight - rect.right; - } else if (!overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else { - left = 0; - } - - if (left != null && top != null) { - alignment = LinkMenuAlignment.bottomRight; - } else if (left != null && bottom != null) { - alignment = LinkMenuAlignment.topRight; - } else if (right != null && top != null) { - alignment = LinkMenuAlignment.bottomLeft; - } else if (right != null && bottom != null) { - alignment = LinkMenuAlignment.topLeft; - } - - return (left, top, right, bottom, alignment); -} - enum LinkMenuAlignment { topLeft, topRight, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart index 898f442891..e087731c82 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -43,5 +44,6 @@ ToolbarItem group1PaddingItem = ToolbarItem group4PaddingItem = buildPaddingPlaceholderItem( 4, - isActive: onlyShowInSingleSelectionAndTextType, + isActive: (state) => + !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index 74198cb46b..d751728526 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -1,8 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,7 +13,8 @@ import 'toolbar_id_enum.dart'; final ToolbarItem customTextAlignItem = ToolbarItem( id: ToolbarId.textAlign.id, group: 4, - isActive: onlyShowInSingleSelectionAndTextType, + isActive: (state) => + !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), builder: ( context, editorState, @@ -33,18 +36,29 @@ class TextAlignActionList extends StatefulWidget { required this.editorState, required this.highlightColor, this.tooltipBuilder, + this.child, + this.onSelect, + this.popoverController, + this.popoverDirection = PopoverDirection.bottomWithLeftAligned, + this.showOffset = const Offset(0, 2), }); final EditorState editorState; final ToolbarTooltipBuilder? tooltipBuilder; final Color highlightColor; + final Widget? child; + final VoidCallback? onSelect; + final PopoverController? popoverController; + final PopoverDirection popoverDirection; + final Offset showOffset; @override State createState() => _TextAlignActionListState(); } class _TextAlignActionListState extends State { - final popoverController = PopoverController(); + late PopoverController popoverController = + widget.popoverController ?? PopoverController(); bool isSelected = false; @@ -62,8 +76,8 @@ class _TextAlignActionListState extends State { Widget build(BuildContext context) { return AppFlowyPopover( controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), + direction: widget.popoverDirection, + offset: widget.showOffset, onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { @@ -72,7 +86,7 @@ class _TextAlignActionListState extends State { keepEditorFocusNotifier.decrease(); }, popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), + child: widget.child ?? buildChild(context), ); } @@ -82,7 +96,7 @@ class _TextAlignActionListState extends State { } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final themeV2 = AFThemeExtensionV2.of(context); final child = FlowyIconButton( width: 48, height: 32, @@ -94,13 +108,13 @@ class _TextAlignActionListState extends State { FlowySvg( FlowySvgs.toolbar_alignment_m, size: Size.square(20), - color: iconColor, + color: themeV2.icon_primary, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: iconColor, + color: themeV2.icon_tertiary, ), ], ), @@ -149,6 +163,7 @@ class _TextAlignActionListState extends State { isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () { command.onAlignChanged(editorState); + widget.onSelect?.call(); popoverController.close(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart index f844080a98..fde59eee51 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -1,8 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports @@ -10,6 +14,10 @@ import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_u import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'custom_text_align_toolbar_item.dart'; +import 'text_suggestions_toolbar_item.dart'; const _kMoreOptionItemId = 'editor.more_option'; const kFontToolbarItemId = 'editor.font'; @@ -54,7 +62,9 @@ class MoreOptionActionList extends StatefulWidget { class _MoreOptionActionListState extends State { final popoverController = PopoverController(); - final fontPopoverController = PopoverController(); + PopoverController fontPopoverController = PopoverController(); + PopoverController suggestionsPopoverController = PopoverController(); + PopoverController textAlignPopoverController = PopoverController(); bool isSelected = false; @@ -62,11 +72,15 @@ class _MoreOptionActionListState extends State { Color get highlightColor => widget.highlightColor; + MoreOptionCommand? tappedCommand; + @override void dispose() { super.dispose(); popoverController.close(); fontPopoverController.close(); + suggestionsPopoverController.close(); + textAlignPopoverController.close(); } @override @@ -154,11 +168,17 @@ class _MoreOptionActionListState extends State { Widget buildPopoverContent() { final showFormula = onlyShowInSingleSelectionAndTextType(editorState); const fontColor = Color(0xff99A1A8); + final isNarrow = isNarrowWindow(editorState); return MouseRegion( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, separatorBuilder: () => const VSpace(4.0), children: [ + if (isNarrow) ...[ + buildTurnIntoSelector(), + buildCommandItem(MoreOptionCommand.link), + buildTextAlignSelector(), + ], buildFontSelector(), buildCommandItem( MoreOptionCommand.strikethrough, @@ -197,6 +217,7 @@ class _MoreOptionActionListState extends State { Widget buildCommandItem( MoreOptionCommand command, { Widget? rightIcon, + VoidCallback? onTap, }) { final isFontCommand = command == MoreOptionCommand.font; return SizedBox( @@ -212,12 +233,14 @@ class _MoreOptionActionListState extends State { figmaLineHeight: 20, fontWeight: FontWeight.w400, ), - onTap: () { - command.onExecute(editorState); - if (command != MoreOptionCommand.font) { - popoverController.close(); - } - }, + onTap: onTap ?? + () { + command.onExecute(editorState, context); + hideOtherPopovers(command); + if (command != MoreOptionCommand.font) { + popoverController.close(); + } + }, ), ); } @@ -255,9 +278,73 @@ class _MoreOptionActionListState extends State { ), ); } + + Widget buildTurnIntoSelector() { + final selectionRects = editorState.selectionRects(); + double height = -6; + if (selectionRects.isNotEmpty) height = selectionRects.first.height; + return SuggestionsActionList( + editorState: editorState, + popoverController: suggestionsPopoverController, + popoverDirection: PopoverDirection.leftWithTopAligned, + showOffset: Offset(-8, height), + onSelect: () => context.read()?.dismiss(), + child: buildCommandItem( + MoreOptionCommand.suggestions, + rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), + onTap: () { + if (tappedCommand == MoreOptionCommand.suggestions) return; + hideOtherPopovers(MoreOptionCommand.suggestions); + keepEditorFocusNotifier.increase(); + suggestionsPopoverController.show(); + }, + ), + ); + } + + Widget buildTextAlignSelector() { + return TextAlignActionList( + editorState: editorState, + popoverController: textAlignPopoverController, + popoverDirection: PopoverDirection.leftWithTopAligned, + showOffset: Offset(-8, 0), + onSelect: () => context.read()?.dismiss(), + highlightColor: highlightColor, + child: buildCommandItem( + MoreOptionCommand.textAlign, + rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), + onTap: () { + if (tappedCommand == MoreOptionCommand.textAlign) return; + hideOtherPopovers(MoreOptionCommand.textAlign); + keepEditorFocusNotifier.increase(); + textAlignPopoverController.show(); + }, + ), + ); + } + + void hideOtherPopovers(MoreOptionCommand currentCommand) { + if (tappedCommand == currentCommand) return; + if (tappedCommand == MoreOptionCommand.font) { + fontPopoverController.close(); + fontPopoverController = PopoverController(); + } else if (tappedCommand == MoreOptionCommand.suggestions) { + suggestionsPopoverController.close(); + suggestionsPopoverController = PopoverController(); + } else if (tappedCommand == MoreOptionCommand.textAlign) { + textAlignPopoverController.close(); + textAlignPopoverController = PopoverController(); + } + tappedCommand = currentCommand; + } } enum MoreOptionCommand { + suggestions(FlowySvgs.turninto_s), + link(FlowySvgs.toolbar_link_m), + textAlign( + FlowySvgs.toolbar_alignment_m, + ), font(FlowySvgs.type_font_m), strikethrough(FlowySvgs.type_strikethrough_m), formula(FlowySvgs.type_formula_m); @@ -268,6 +355,12 @@ enum MoreOptionCommand { String get title { switch (this) { + case suggestions: + return LocaleKeys.document_toolbar_turnInto.tr(); + case link: + return LocaleKeys.document_toolbar_link.tr(); + case textAlign: + return LocaleKeys.button_align.tr(); case font: return LocaleKeys.document_toolbar_font.tr(); case strikethrough: @@ -277,14 +370,26 @@ enum MoreOptionCommand { } } - Future onExecute(EditorState editorState) async { - if (this == strikethrough) { + Future onExecute(EditorState editorState, BuildContext context) async { + final selection = editorState.selection!; + if (this == link) { + final nodes = editorState.getNodesInSelection(selection); + final isHref = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[AppFlowyRichTextKeys.href] != null, + ); + }); + context.read()?.dismiss(); + if (isHref) { + getIt().call( + HoverTriggerKey(nodes.first.id, selection), + ); + } else { + showLinkCreateMenu(context, editorState, selection); + } + } else if (this == strikethrough) { await editorState.toggleAttribute(name); } else if (this == formula) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return; - } final node = editorState.getNodeAtPath(selection.start.path); final delta = node?.delta; if (node == null || delta == null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 4367ddb489..625fedff79 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -77,7 +78,7 @@ class _TextHeadingActionListState extends State { } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final themeV2 = AFThemeExtensionV2.of(context); final child = FlowyIconButton( width: 48, height: 32, @@ -89,13 +90,13 @@ class _TextHeadingActionListState extends State { FlowySvg( FlowySvgs.toolbar_text_format_m, size: Size.square(20), - color: iconColor, + color: themeV2.icon_primary, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: iconColor, + color: themeV2.icon_tertiary, ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index 97245297b5..c7f3b513f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -1,14 +1,16 @@ import 'dart:collection'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -45,17 +47,28 @@ class SuggestionsActionList extends StatefulWidget { super.key, required this.editorState, this.tooltipBuilder, + this.child, + this.onSelect, + this.popoverController, + this.popoverDirection = PopoverDirection.bottomWithLeftAligned, + this.showOffset = const Offset(0, 2), }); final EditorState editorState; final ToolbarTooltipBuilder? tooltipBuilder; + final Widget? child; + final VoidCallback? onSelect; + final PopoverController? popoverController; + final PopoverDirection popoverDirection; + final Offset showOffset; @override State createState() => _SuggestionsActionListState(); } class _SuggestionsActionListState extends State { - final popoverController = PopoverController(); + late PopoverController popoverController = + widget.popoverController ?? PopoverController(); bool isSelected = false; @@ -83,8 +96,8 @@ class _SuggestionsActionListState extends State { Widget build(BuildContext context) { return AppFlowyPopover( controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), + direction: widget.popoverDirection, + offset: widget.showOffset, onOpen: () => keepEditorFocusNotifier.increase(), onClose: () { setState(() { @@ -94,7 +107,7 @@ class _SuggestionsActionListState extends State { }, constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400), popupBuilder: (context) => buildPopoverContent(context), - child: buildChild(context), + child: widget.child ?? buildChild(context), ); } @@ -104,7 +117,8 @@ class _SuggestionsActionListState extends State { } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final themeV2 = AFThemeExtensionV2.of(context); + final child = FlowyHover( isSelected: () => isSelected, style: HoverStyle( @@ -147,7 +161,7 @@ class _SuggestionsActionListState extends State { FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: iconColor, + color: themeV2.icon_tertiary, ), ], ), @@ -206,7 +220,8 @@ class _SuggestionsActionListState extends State { ), rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () { - item.onTap(widget.editorState); + item.onTap(widget.editorState, true); + widget.onSelect?.call(); popoverController.close(); }, ), @@ -289,10 +304,10 @@ class SuggestionItem { final SuggestionType type; final String title; final FlowySvgData svg; - final ValueChanged onTap; + final Function(EditorState state, bool keepSelection) onTap; } -enum SuggestionGroup { textHeading, list, toggle, quote } +enum SuggestionGroup { textHeading, list, toggle, quote, page } enum SuggestionType { text(SuggestionGroup.textHeading), @@ -307,7 +322,8 @@ enum SuggestionType { toggleH2(SuggestionGroup.toggle), toggleH3(SuggestionGroup.toggle), callOut(SuggestionGroup.quote), - quote(SuggestionGroup.quote); + quote(SuggestionGroup.quote), + page(SuggestionGroup.page); const SuggestionType(this.group); @@ -318,94 +334,166 @@ final textSuggestionItem = SuggestionItem( type: SuggestionType.text, title: AppFlowyEditorL10n.current.text, svg: FlowySvgs.type_text_m, - onTap: (state) => formatNodeToText(state), + onTap: (state, _) => formatNodeToText(state), ); final h1SuggestionItem = SuggestionItem( type: SuggestionType.h1, title: LocaleKeys.document_toolbar_h1.tr(), svg: FlowySvgs.type_h1_m, - onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 1), + onTap: (state, keepSelection) => _turnInto( + state, + HeadingBlockKeys.type, + level: 1, + keepSelection: keepSelection, + ), ); final h2SuggestionItem = SuggestionItem( type: SuggestionType.h2, title: LocaleKeys.document_toolbar_h2.tr(), svg: FlowySvgs.type_h2_m, - onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 2), + onTap: (state, keepSelection) => _turnInto( + state, + HeadingBlockKeys.type, + level: 2, + keepSelection: keepSelection, + ), ); final h3SuggestionItem = SuggestionItem( type: SuggestionType.h3, title: LocaleKeys.document_toolbar_h3.tr(), svg: FlowySvgs.type_h3_m, - onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 3), + onTap: (state, keepSelection) => _turnInto( + state, + HeadingBlockKeys.type, + level: 3, + keepSelection: keepSelection, + ), ); final checkboxSuggestionItem = SuggestionItem( type: SuggestionType.checkbox, title: LocaleKeys.editor_checkbox.tr(), svg: FlowySvgs.type_todo_m, - onTap: (state) => _turnInto(state, TodoListBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + TodoListBlockKeys.type, + keepSelection: keepSelection, + ), ); final bulletedSuggestionItem = SuggestionItem( type: SuggestionType.bulleted, title: LocaleKeys.editor_bulletedListShortForm.tr(), svg: FlowySvgs.type_bulleted_list_m, - onTap: (state) => _turnInto(state, BulletedListBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + BulletedListBlockKeys.type, + keepSelection: keepSelection, + ), ); final numberedSuggestionItem = SuggestionItem( type: SuggestionType.numbered, title: LocaleKeys.editor_numberedListShortForm.tr(), svg: FlowySvgs.type_numbered_list_m, - onTap: (state) => _turnInto(state, NumberedListBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + NumberedListBlockKeys.type, + keepSelection: keepSelection, + ), ); final toggleSuggestionItem = SuggestionItem( type: SuggestionType.toggle, title: LocaleKeys.editor_toggleListShortForm.tr(), svg: FlowySvgs.type_toggle_list_m, - onTap: (state) => _turnInto(state, ToggleListBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + ToggleListBlockKeys.type, + keepSelection: keepSelection, + ), ); final toggleH1SuggestionItem = SuggestionItem( type: SuggestionType.toggleH1, title: LocaleKeys.editor_toggleHeading1ShortForm.tr(), svg: FlowySvgs.type_toggle_h1_m, - onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 1), + onTap: (state, keepSelection) => _turnInto( + state, + ToggleListBlockKeys.type, + level: 1, + keepSelection: keepSelection, + ), ); final toggleH2SuggestionItem = SuggestionItem( type: SuggestionType.toggleH2, title: LocaleKeys.editor_toggleHeading2ShortForm.tr(), svg: FlowySvgs.type_toggle_h2_m, - onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 2), + onTap: (state, keepSelection) => _turnInto( + state, + ToggleListBlockKeys.type, + level: 2, + keepSelection: keepSelection, + ), ); final toggleH3SuggestionItem = SuggestionItem( type: SuggestionType.toggleH3, title: LocaleKeys.editor_toggleHeading3ShortForm.tr(), svg: FlowySvgs.type_toggle_h3_m, - onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 3), + onTap: (state, keepSelection) => _turnInto( + state, + ToggleListBlockKeys.type, + level: 3, + keepSelection: keepSelection, + ), ); final callOutSuggestionItem = SuggestionItem( type: SuggestionType.callOut, title: LocaleKeys.document_plugins_callout.tr(), svg: FlowySvgs.type_callout_m, - onTap: (state) => _turnInto(state, CalloutBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + CalloutBlockKeys.type, + keepSelection: keepSelection, + ), ); final quoteSuggestionItem = SuggestionItem( type: SuggestionType.quote, title: LocaleKeys.editor_quote.tr(), svg: FlowySvgs.type_quote_m, - onTap: (state) => _turnInto(state, QuoteBlockKeys.type), + onTap: (state, keepSelection) => _turnInto( + state, + QuoteBlockKeys.type, + keepSelection: keepSelection, + ), ); -Future _turnInto(EditorState state, String type, {int? level}) async { +final pateItem = SuggestionItem( + type: SuggestionType.page, + title: LocaleKeys.editor_page.tr(), + svg: FlowySvgs.icon_document_s, + onTap: (state, keepSelection) => _turnInto( + state, + SubPageBlockKeys.type, + viewId: getIt().latestOpenView?.id, + keepSelection: keepSelection, + ), +); + +Future _turnInto( + EditorState state, + String type, { + int? level, + String? viewId, + bool keepSelection = true, +}) async { final selection = state.selection!; final node = state.getNodeAtPath(selection.start.path)!; await BlockActionOptionCubit.turnIntoBlock( @@ -413,7 +501,8 @@ Future _turnInto(EditorState state, String type, {int? level}) async { node, state, level: level, - keepSelection: true, + currentViewId: viewId, + keepSelection: keepSelection, ); } @@ -431,6 +520,7 @@ final suggestions = UnmodifiableListView([ toggleH3SuggestionItem, callOutSuggestionItem, quoteSuggestionItem, + pateItem, ]); final nodeType2SuggestionType = UnmodifiableMapView({ diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 2e9b2c9afd..bf5dcfdafc 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/util/expand_views.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; @@ -185,6 +186,7 @@ Future initGetIt( ); getIt.registerSingleton(PluginSandbox()); getIt.registerSingleton(ViewExpanderRegistry()); + getIt.registerSingleton(LinkHoverTriggers()); await DependencyResolver.resolve(getIt, mode); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 0b5c0c5f98..568fd76db0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -2,6 +2,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @@ -14,9 +15,8 @@ class DesktopAppearance extends BaseAppearance { ) { assert(codeFontFamily.isNotEmpty); - final theme = brightness == Brightness.light - ? appTheme.lightTheme - : appTheme.darkTheme; + final isLight = brightness == Brightness.light; + final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; final colorScheme = ColorScheme( brightness: brightness, @@ -152,6 +152,11 @@ class DesktopAppearance extends BaseAppearance { lightIconColor: theme.lightIconColor, toolbarHoverColor: theme.toolbarHoverColor, ), + isLight + ? lightAFThemeV2 + : darkAFThemeV2.copyWith( + icon_primary: theme.icon, + ), ], ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart new file mode 100644 index 0000000000..7fac9e07d0 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart @@ -0,0 +1,91 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:flutter/material.dart'; + +@immutable +class AFThemeExtensionV2 extends ThemeExtension { + static AFThemeExtensionV2 of(BuildContext context) => + Theme.of(context).extension()!; + + static AFThemeExtensionV2? maybeOf(BuildContext context) => + Theme.of(context).extension(); + + const AFThemeExtensionV2({ + required this.icon_primary, + required this.icon_tertiary, + required this.border_grey_quaternary, + required this.fill_theme_select, + required this.fill_grey_thick_alpha_1, + required this.shadow_medium, + }); + + final Color icon_primary; + final Color icon_tertiary; + final Color border_grey_quaternary; + final Color fill_theme_select; + final Color fill_grey_thick_alpha_1; + final Color shadow_medium; + + @override + AFThemeExtensionV2 copyWith({ + Color? icon_primary, + Color? icon_tertiary, + Color? border_grey_quaternary, + Color? fill_theme_select, + Color? fill_grey_thick_alpha_1, + Color? shadow_medium, + }) => + AFThemeExtensionV2( + icon_primary: icon_primary ?? this.icon_primary, + icon_tertiary: icon_tertiary ?? this.icon_tertiary, + border_grey_quaternary: + border_grey_quaternary ?? this.border_grey_quaternary, + fill_theme_select: fill_theme_select ?? this.fill_theme_select, + fill_grey_thick_alpha_1: + fill_grey_thick_alpha_1 ?? this.fill_grey_thick_alpha_1, + shadow_medium: shadow_medium ?? this.shadow_medium, + ); + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! AFThemeExtensionV2) { + return this; + } + return AFThemeExtensionV2( + icon_primary: + Color.lerp(icon_primary, other.icon_primary, t) ?? icon_primary, + icon_tertiary: + Color.lerp(icon_tertiary, other.icon_tertiary, t) ?? icon_tertiary, + border_grey_quaternary: + Color.lerp(border_grey_quaternary, other.border_grey_quaternary, t) ?? + border_grey_quaternary, + fill_theme_select: + Color.lerp(fill_theme_select, other.fill_theme_select, t) ?? + fill_theme_select, + fill_grey_thick_alpha_1: Color.lerp( + fill_grey_thick_alpha_1, other.fill_grey_thick_alpha_1, t) ?? + fill_grey_thick_alpha_1, + shadow_medium: + Color.lerp(shadow_medium, other.shadow_medium, t) ?? shadow_medium, + ); + } +} + +const AFThemeExtensionV2 darkAFThemeV2 = AFThemeExtensionV2( + icon_primary: Color(0xFF1F2329), + icon_tertiary: Color(0xFF99A1A8), + border_grey_quaternary: Color(0xFFE8ECF3), + fill_theme_select: Color(0x00BCF01F), + fill_grey_thick_alpha_1: Color(0x1F23290F), + shadow_medium: Color(0x1F22251F), +); + +const AFThemeExtensionV2 lightAFThemeV2 = AFThemeExtensionV2( + icon_primary: Color(0xFF1F2329), + icon_tertiary: Color(0xFF99A1A8), + border_grey_quaternary: Color(0xFFE8ECF3), + fill_theme_select: Color(0x00BCF01F), + fill_grey_thick_alpha_1: Color(0x1F23290F), + shadow_medium: Color(0x1F22251F), +); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index e468ba89b4..1e156a4530 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8f314fd" - resolved-ref: "8f314fda5981e650a52ba522ba7915e13940d837" + ref: "5ad9d77" + resolved-ref: "5ad9d771a8496dea95a9c5b1ec77f76df3983037" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 48d380a622..f623932a16 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -184,7 +184,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "8f314fd" + ref: "5ad9d77" appflowy_editor_plugins: git: From 07a78b4ad75e931a979ddb1c2f3c5e34c803088f Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:26:45 +0800 Subject: [PATCH 230/384] chore: adjust default model name (#7634) --- .../ai/service/ai_model_state_notifier.dart | 10 ++++++ .../prompt_input/select_model_menu.dart | 36 +++++++++++++------ .../ai_writer_prompt_input_more_button.dart | 7 ---- .../setting_ai_view/model_selection.dart | 3 +- frontend/resources/translations/en.json | 3 +- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index c43bd01c6f..c65913cf19 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -170,3 +170,13 @@ class AIModelStateNotifier { ); } } + +extension AiModelExtension on AIModelPB { + bool get isDefault { + return name == "Default"; + } + + String get i18n { + return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name; + } +} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index b9e3daada9..1860bd7d63 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -55,7 +55,7 @@ class _SelectModelMenuState extends State { ); }, child: _CurrentModelButton( - modelName: state.selectedModel!.name, + model: state.selectedModel!, onTap: () => popoverController.show(), ), ); @@ -166,7 +166,10 @@ class _ModelItem extends StatelessWidget { child: FlowyButton( onTap: onTap, margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: FlowyText(model.name), + text: FlowyText( + model.i18n, + overflow: TextOverflow.ellipsis, + ), rightIcon: isSelected ? FlowySvg(FlowySvgs.check_s) : null, ), ); @@ -175,11 +178,11 @@ class _ModelItem extends StatelessWidget { class _CurrentModelButton extends StatelessWidget { const _CurrentModelButton({ - required this.modelName, + required this.model, required this.onTap, }); - final String modelName; + final AIModelPB model; final VoidCallback onTap; @override @@ -199,13 +202,26 @@ class _CurrentModelButton extends StatelessWidget { padding: const EdgeInsetsDirectional.all(4.0), child: Row( children: [ - FlowyText( - modelName, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, + Padding( + // TODO: remove this after change icon to 20px + padding: EdgeInsets.all(2), + child: FlowySvg( + FlowySvgs.ai_sparks_s, + color: Theme.of(context).hintColor, + size: Size.square(16), + ), ), - HSpace(2.0), + if (!model.isDefault) + Padding( + padding: EdgeInsetsDirectional.only(end: 2.0), + child: FlowyText( + model.i18n, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), FlowySvg( FlowySvgs.ai_source_drop_down_s, color: Theme.of(context).hintColor, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart index 1bd80d081d..75c6589326 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart @@ -42,13 +42,6 @@ class AiWriterPromptMoreButton extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - FlowySvg( - FlowySvgs.ai_sparks_s, - color: isEnabled - ? Theme.of(context).hintColor - : Theme.of(context).disabledColor, - ), - const HSpace(2.0), FlowyText( LocaleKeys.ai_more.tr(), fontSize: 12, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index bec4013a36..3df5ad3987 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/ai/ai.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:flutter/material.dart'; @@ -44,7 +45,7 @@ class AIModelSelection extends StatelessWidget { (model) => buildDropdownMenuEntry( context, value: model, - label: model.name, + label: model.i18n, ), ) .toList(), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 24f734af5d..429f1ea8c3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -255,7 +255,8 @@ "switchModel": { "label": "Switch model", "localModel": "Local Model", - "cloudModel": "Cloud Model" + "cloudModel": "Cloud Model", + "autoModel": "Auto" }, "selectBanner": { "saveButton": "Add to …", From d3483618890aacf2e77d397844841311de19294b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 16:31:18 +0800 Subject: [PATCH 231/384] chore: return desc of ai model --- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++++++++++---------- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 28 ++++++++++-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 35 ++++++++------- frontend/rust-lib/flowy-ai/src/entities.rs | 33 +++----------- 4 files changed, 71 insertions(+), 71 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 29cef4afde..f0ff1df6cb 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,3 +1,4 @@ +use crate::cloud::ai_dto::AvailableModel; pub use client_api::entity::ai_dto::{ AppFlowyOfflineAI, CompleteTextParams, CompletionMessage, CompletionMetadata, CompletionType, CreateChatContext, CustomPrompt, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, @@ -27,29 +28,50 @@ pub type StreamComplete = BoxStream<'static, Result for AIModel { + fn from(value: AvailableModel) -> Self { + let desc = value + .metadata + .as_ref() + .and_then(|v| v.get("desc").map(|v| v.as_str().unwrap_or(""))) + .unwrap_or(""); + Self { + name: value.name, + is_local: false, + desc: desc.to_string(), + } + } } impl AIModel { - pub fn server(name: String) -> Self { + pub fn server(name: String, desc: String) -> Self { Self { name, is_local: false, + desc, } } - pub fn local(name: String) -> Self { + pub fn local(name: String, desc: String) -> Self { Self { name, is_local: true, + desc, } } } +const DEFAULT_MODEL_NAME: &str = "Auto"; impl Default for AIModel { fn default() -> Self { Self { - name: "Auto".to_string(), + name: DEFAULT_MODEL_NAME.to_string(), is_local: false, + desc: "".to_string(), } } } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 0a63d8ece0..6cea59516e 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -361,20 +361,18 @@ impl AIManager { if enabled { if let Some(name) = self.local_ai.get_plugin_chat_model() { info!("Set global active model to local ai: {}", name); - let model = AIModel::local(name); + let model = AIModel::local(name, "".to_string()); self.update_selected_model(source_key, model).await?; } } else { info!("Set global active model to default"); - let global_active_model = self - .get_workspace_select_model() - .await - .map(AIModel::server) - .unwrap_or_else(|_| AIModel::default()); - - self - .update_selected_model(source_key, global_active_model) - .await?; + let global_active_model = self.get_workspace_select_model().await?; + let models = self.get_server_available_models().await?; + if let Some(model) = models.into_iter().find(|m| m.name == global_active_model) { + self + .update_selected_model(source_key, AIModel::from(model)) + .await?; + } } Ok(()) @@ -382,21 +380,21 @@ impl AIManager { pub async fn get_available_models(&self, source: String) -> FlowyResult { // Build the models list from server models and mark them as non-local. - let mut models: Vec = self + let mut models: Vec = self .get_server_available_models() .await? .into_iter() - .map(|m| AIModelPB::server(m.name)) + .map(|m| AIModel::from(m)) .collect(); // If user enable local ai, then add local ai model to the list. if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - models.push(AIModelPB::local(local_model)); + models.push(AIModel::local(local_model, "".to_string())); } if models.is_empty() { return Ok(AvailableModelsPB { - models, + models: models.into_iter().map(|m| m.into()).collect(), selected_model: AIModelPB::default(), }); } @@ -405,7 +403,7 @@ impl AIManager { let global_active_model = self .get_workspace_select_model() .await - .map(AIModel::server) + .map(|m| AIModel::server(m, "".to_string())) .unwrap_or_else(|_| AIModel::default()); let mut user_selected_model = global_active_model.clone(); @@ -430,7 +428,7 @@ impl AIManager { .iter() .find(|m| m.name == user_selected_model.name) .cloned() - .or_else(|| Some(AIModelPB::from(global_active_model))); + .or_else(|| Some(AIModel::from(global_active_model))); // Update the stored preference if a different model is used. if let Some(ref active_model) = active_model { @@ -441,9 +439,10 @@ impl AIManager { } } + let selected_model = AIModelPB::from(active_model.unwrap_or_default()); Ok(AvailableModelsPB { - models, - selected_model: active_model.unwrap_or_default(), + models: models.into_iter().map(|m| m.into()).collect(), + selected_model, }) } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index d10aa950e3..a0116a3388 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -200,19 +200,9 @@ pub struct AvailableModelPB { #[pb(index = 2)] pub is_default: bool, -} -impl From for AvailableModelPB { - fn from(value: AvailableModel) -> Self { - let is_default = value - .metadata - .and_then(|v| v.get("is_default").map(|v| v.as_bool().unwrap_or(false))) - .unwrap_or(false); - Self { - name: value.name, - is_default, - } - } + #[pb(index = 3)] + pub desc: String, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -248,22 +238,9 @@ pub struct AIModelPB { #[pb(index = 2)] pub is_local: bool, -} -impl AIModelPB { - pub fn server(name: String) -> Self { - Self { - name, - is_local: false, - } - } - - pub fn local(name: String) -> Self { - Self { - name, - is_local: true, - } - } + #[pb(index = 3)] + pub desc: String, } impl From for AIModelPB { @@ -271,6 +248,7 @@ impl From for AIModelPB { Self { name: model.name, is_local: model.is_local, + desc: model.desc, } } } @@ -280,6 +258,7 @@ impl From for AIModel { AIModel { name: value.name, is_local: value.is_local, + desc: value.desc, } } } From 7ee29dcbc524cc973421ae0caab7cf96c2387a23 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 17:09:01 +0800 Subject: [PATCH 232/384] chore: return model desc --- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 4 ++-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 23 ++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index f0ff1df6cb..7eb8386de1 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -65,11 +65,11 @@ impl AIModel { } } -const DEFAULT_MODEL_NAME: &str = "Auto"; +pub const DEFAULT_AI_MODEL_NAME: &str = "Auto"; impl Default for AIModel { fn default() -> Self { Self { - name: DEFAULT_MODEL_NAME.to_string(), + name: DEFAULT_AI_MODEL_NAME.to_string(), is_local: false, desc: "".to_string(), } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 6cea59516e..6092839747 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -10,7 +10,9 @@ use std::collections::HashMap; use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_ai_pub::cloud::{AIModel, ChatCloudService, ChatSettings, UpdateChatParams}; +use flowy_ai_pub::cloud::{ + AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, +}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; @@ -275,6 +277,10 @@ impl AIManager { .cloud_service_wm .get_workspace_default_model(&workspace_id) .await?; + + if model.is_empty() { + return Ok(DEFAULT_AI_MODEL_NAME.to_string()); + } Ok(model) } @@ -387,6 +393,8 @@ impl AIManager { .map(|m| AIModel::from(m)) .collect(); + trace!("[Model Selection]: Available models: {:?}", models); + // If user enable local ai, then add local ai model to the list. if let Some(local_model) = self.local_ai.get_plugin_chat_model() { models.push(AIModel::local(local_model, "".to_string())); @@ -400,13 +408,18 @@ impl AIManager { } // Global active model is the model selected by the user in the workspace settings. - let global_active_model = self + let server_active_model = self .get_workspace_select_model() .await .map(|m| AIModel::server(m, "".to_string())) .unwrap_or_else(|_| AIModel::default()); - let mut user_selected_model = global_active_model.clone(); + trace!( + "[Model Selection] server active model: {:?}", + server_active_model + ); + + let mut user_selected_model = server_active_model.clone(); let source_key = ai_available_models_key(&source); // If source is provided, try to get the user-selected model from the store. User selected @@ -419,6 +432,7 @@ impl AIManager { } }, Some(model) => { + trace!("[Model Selection] user select model: {:?}", model); user_selected_model = model; }, } @@ -428,7 +442,7 @@ impl AIManager { .iter() .find(|m| m.name == user_selected_model.name) .cloned() - .or_else(|| Some(AIModel::from(global_active_model))); + .or_else(|| Some(AIModel::from(server_active_model))); // Update the stored preference if a different model is used. if let Some(ref active_model) = active_model { @@ -439,6 +453,7 @@ impl AIManager { } } + trace!("[Model Selection] final active model: {:?}", active_model); let selected_model = AIModelPB::from(active_model.unwrap_or_default()); Ok(AvailableModelsPB { models: models.into_iter().map(|m| m.into()).collect(), From f574b6b9c297f0494edae52045c340963b6333da Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 17:10:35 +0800 Subject: [PATCH 233/384] chore: update default name --- .../lib/ai/service/ai_model_state_notifier.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index c65913cf19..80ff8d630e 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -173,7 +173,7 @@ class AIModelStateNotifier { extension AiModelExtension on AIModelPB { bool get isDefault { - return name == "Default"; + return name == "Auto"; } String get i18n { From b83b9646780dca302d6a36992d6af5980b6befc9 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:01:45 +0800 Subject: [PATCH 234/384] chore: add model description UI --- .../prompt_input/select_model_menu.dart | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index 1860bd7d63..64a62b902c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -161,16 +161,37 @@ class _ModelItem extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - height: 32, + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 32), child: FlowyButton( onTap: onTap, margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: FlowyText( - model.i18n, - overflow: TextOverflow.ellipsis, + text: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + model.i18n, + figmaLineHeight: 20, + overflow: TextOverflow.ellipsis, + color: isSelected ? Theme.of(context).colorScheme.primary : null, + ), + if (model.desc.isNotEmpty) + FlowyText( + model.desc, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ], ), - rightIcon: isSelected ? FlowySvg(FlowySvgs.check_s) : null, + rightIcon: isSelected + ? FlowySvg( + FlowySvgs.check_s, + size: const Size.square(20), + color: Theme.of(context).colorScheme.primary, + ) + : null, ), ); } From f6f19a0a078cf02b510a1c777ea75c30692f4dd6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 19:17:07 +0800 Subject: [PATCH 235/384] chore: display ai model desc on setting page --- .../setting_ai_view/model_selection.dart | 2 ++ .../shared/af_dropdown_menu_entry.dart | 35 +++++++++++++++---- frontend/rust-lib/flowy-ai/src/entities.rs | 1 - 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 3df5ad3987..2c4c4fa920 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -46,6 +46,8 @@ class AIModelSelection extends StatelessWidget { context, value: model, label: model.i18n, + subLabel: model.desc, + maximumHeight: 49, ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index d005901cff..1d1e58626c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -9,14 +9,40 @@ DropdownMenuEntry buildDropdownMenuEntry( BuildContext context, { required T value, required String label, + String? subLabel, T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, String? fontFamily, + double maximumHeight = 29, }) { final fontFamilyUsed = fontFamily != null ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily : defaultFontFamily; + Widget? labelWidget; + if (subLabel != null) { + labelWidget = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + label, + fontSize: 14, + ), + const VSpace(4), + FlowyText.regular( + subLabel, + fontSize: 10, + ), + ], + ); + } else { + labelWidget = FlowyText.regular( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ); + } return DropdownMenuEntry( style: ButtonStyle( @@ -26,17 +52,12 @@ DropdownMenuEntry buildDropdownMenuEntry( const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), - maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), + maximumSize: WidgetStatePropertyAll(Size(double.infinity, maximumHeight)), ), value: value, label: label, leadingIcon: leadingWidget, - labelWidget: FlowyText.regular( - label, - fontSize: 14, - textAlign: TextAlign.start, - fontFamily: fontFamilyUsed, - ), + labelWidget: labelWidget, trailingIcon: Row( children: [ if (trailingWidget != null) ...[ diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index a0116a3388..353ff9d0a6 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; -use flowy_ai_pub::cloud::ai_dto::AvailableModel; use flowy_ai_pub::cloud::{ AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, From ff70595a15f7ef7c17f0c4323d6503070f0bfe37 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 20:03:07 +0800 Subject: [PATCH 236/384] chore: display ai model desc and fix flashing when select model --- .../settings/ai/settings_ai_bloc.dart | 24 +++- .../setting_ai_view/model_selection.dart | 10 +- .../setting_ai_view/settings_ai_view.dart | 24 ++-- .../settings/settings_dialog.dart | 2 +- .../shared/af_dropdown_menu_entry.dart | 4 +- .../flowy-codegen/src/protobuf_file/mod.rs | 116 +++++++++--------- 6 files changed, 100 insertions(+), 80 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 1d50cde480..4383e0dbef 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -11,13 +11,15 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; +const String aiModelsGlobalActiveModel = "ai_models_global_active_model"; + class SettingsAIBloc extends Bloc { SettingsAIBloc( this.userProfile, this.workspaceId, ) : _userListener = UserListener(userProfile: userProfile), _aiModelSwitchListener = - AIModelSwitchListener(objectId: "ai_models_global_active_model"), + AIModelSwitchListener(objectId: aiModelsGlobalActiveModel), super( SettingsAIState( userProfile: userProfile, @@ -58,6 +60,7 @@ class SettingsAIBloc extends Bloc { }, ); _loadModelList(); + _loadUserWorkspaceSetting(); }, didReceiveUserProfile: (userProfile) { emit(state.copyWith(userProfile: userProfile)); @@ -75,6 +78,12 @@ class SettingsAIBloc extends Bloc { if (!model.isLocal) { await _updateUserWorkspaceSetting(model: model.name); } + await AIEventUpdateSelectedModel( + UpdateSelectedModelPB( + source: aiModelsGlobalActiveModel, + selectedModel: model, + ), + ).send(); }, didLoadAISetting: (UseAISettingPB settings) { emit( @@ -135,6 +144,19 @@ class SettingsAIBloc extends Bloc { }); }); } + + void _loadUserWorkspaceSetting() { + final payload = UserWorkspaceIdPB(workspaceId: workspaceId); + UserEventGetWorkspaceSetting(payload).send().then((result) { + result.fold((settings) { + if (!isClosed) { + add(SettingsAIEvent.didLoadAISetting(settings)); + } + }, (err) { + Log.error(err); + }); + }); + } } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 2c4c4fa920..05db831e04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -12,13 +12,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class AIModelSelection extends StatelessWidget { const AIModelSelection({super.key}); + static const double height = 49; @override Widget build(BuildContext context) { return BlocBuilder( + buildWhen: (previous, current) => + previous.availableModels != current.availableModels, builder: (context, state) { if (state.availableModels == null) { - return const SizedBox.shrink(); + return const SizedBox( + // Using same height as SettingsDropdown to avoid layout shift + height: height, + ); } return Padding( @@ -47,7 +53,7 @@ class AIModelSelection extends StatelessWidget { value: model, label: model.i18n, subLabel: model.desc, - maximumHeight: 49, + maximumHeight: height, ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 0fc1ef293c..efb969700e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -44,22 +44,14 @@ class SettingsAIView extends StatelessWidget { return BlocProvider( create: (_) => SettingsAIBloc(userProfile, workspaceId) ..add(const SettingsAIEvent.started()), - child: BlocBuilder( - builder: (context, state) { - final children = [ - const AIModelSelection(), - ]; - - children.add(const _AISearchToggle(value: false)); - children.add(const LocalAISetting()); - - return SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: - LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: children, - ); - }, + child: SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), + children: [ + const AIModelSelection(), + const _AISearchToggle(value: false), + const LocalAISetting(), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 5165744627..2cf83276b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -141,7 +141,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.ai: if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { return SettingsAIView( - key: ValueKey(user.hashCode), + key: ValueKey(workspaceId), userProfile: user, currentWorkspaceMemberRole: currentWorkspaceMemberRole, workspaceId: workspaceId, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index 1d1e58626c..720f7793f2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -9,7 +9,7 @@ DropdownMenuEntry buildDropdownMenuEntry( BuildContext context, { required T value, required String label, - String? subLabel, + String subLabel = '', T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, @@ -20,7 +20,7 @@ DropdownMenuEntry buildDropdownMenuEntry( ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily : defaultFontFamily; Widget? labelWidget; - if (subLabel != null) { + if (subLabel.isNotEmpty) { labelWidget = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs index 8baa78c096..677e7bcddf 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs @@ -76,64 +76,64 @@ pub fn dart_gen(crate_name: &str) { } } -#[allow(unused_variables)] -fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { - // 1. generate the proto files to proto_file_dir - #[cfg(feature = "proto_gen")] - let proto_crates = gen_proto_files(crate_name); - - for proto_crate in proto_crates { - let mut proto_file_paths = vec![]; - let mut file_names = vec![]; - let proto_file_output_path = proto_crate - .proto_output_path() - .to_str() - .unwrap() - .to_string(); - let protobuf_output_path = proto_crate - .protobuf_crate_path() - .to_str() - .unwrap() - .to_string(); - - for (path, file_name) in WalkDir::new(&proto_file_output_path) - .into_iter() - .filter_map(|e| e.ok()) - .map(|e| { - let path = e.path().to_str().unwrap().to_string(); - let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); - (path, file_name) - }) - { - if path.ends_with(".proto") { - // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project - println!("cargo:rerun-if-changed={}", path); - proto_file_paths.push(path); - file_names.push(file_name); - } - } - let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); - - // 2. generate the protobuf files(Dart) - #[cfg(feature = "ts")] - generate_ts_protobuf_files( - dest_folder_name, - &proto_file_output_path, - &proto_file_paths, - &file_names, - &protoc_bin_path, - &project, - ); - - // 3. generate the protobuf files(Rust) - generate_rust_protobuf_files( - &protoc_bin_path, - &proto_file_paths, - &proto_file_output_path, - &protobuf_output_path, - ); - } -} +// #[allow(unused_variables)] +// fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { +// // 1. generate the proto files to proto_file_dir +// #[cfg(feature = "proto_gen")] +// let proto_crates = gen_proto_files(crate_name); +// +// for proto_crate in proto_crates { +// let mut proto_file_paths = vec![]; +// let mut file_names = vec![]; +// let proto_file_output_path = proto_crate +// .proto_output_path() +// .to_str() +// .unwrap() +// .to_string(); +// let protobuf_output_path = proto_crate +// .protobuf_crate_path() +// .to_str() +// .unwrap() +// .to_string(); +// +// for (path, file_name) in WalkDir::new(&proto_file_output_path) +// .into_iter() +// .filter_map(|e| e.ok()) +// .map(|e| { +// let path = e.path().to_str().unwrap().to_string(); +// let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); +// (path, file_name) +// }) +// { +// if path.ends_with(".proto") { +// // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project +// println!("cargo:rerun-if-changed={}", path); +// proto_file_paths.push(path); +// file_names.push(file_name); +// } +// } +// let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); +// +// // 2. generate the protobuf files(Dart) +// #[cfg(feature = "ts")] +// generate_ts_protobuf_files( +// dest_folder_name, +// &proto_file_output_path, +// &proto_file_paths, +// &file_names, +// &protoc_bin_path, +// &project, +// ); +// +// // 3. generate the protobuf files(Rust) +// generate_rust_protobuf_files( +// &protoc_bin_path, +// &proto_file_paths, +// &proto_file_output_path, +// &protobuf_output_path, +// ); +// } +// } fn generate_rust_protobuf_files( protoc_bin_path: &Path, From 8cf31b8afc5557606a74310e70966ef4e45b4ed5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 20:51:29 +0800 Subject: [PATCH 237/384] chore: bump client api --- .../ai/service/ai_model_state_notifier.dart | 1 - frontend/rust-lib/Cargo.lock | 40 +++++++++---------- frontend/rust-lib/Cargo.toml | 20 +++++----- .../src/middleware/chat_service_mw.rs | 2 +- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index 80ff8d630e..0bcc41da9b 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -113,7 +113,6 @@ class AIModelStateNotifier { final localAiState = _localAIState; if (availableModels == null) { - Log.warn("No available models"); return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); } if (localAiState == null) { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ba07ff9c3b..0e800af7ed 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "bytes", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "again", "anyhow", @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "collab-entity", "collab-rt-entity", @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "futures-channel", "futures-util", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "arc-swap", @@ -924,7 +924,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "async-trait", @@ -964,7 +964,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "arc-swap", @@ -985,7 +985,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "bytes", @@ -1005,7 +1005,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "arc-swap", @@ -1027,7 +1027,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "async-recursion", @@ -1091,7 +1091,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "async-stream", @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "bincode", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "async-trait", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" dependencies = [ "anyhow", "collab", @@ -1546,7 +1546,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "bincode", "bytes", @@ -2980,7 +2980,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -2995,7 +2995,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "app-error", "jsonwebtoken", @@ -3610,7 +3610,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "bytes", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=dfd44f8fb7152ace03eb03e46391cf76266948b9#dfd44f8fb7152ace03eb03e46391cf76266948b9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 843a939ae3..269cabb938 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "dfd44f8fb7152ace03eb03e46391cf76266948b9" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "dfd44f8fb7152ace03eb03e46391cf76266948b9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" } [profile.dev] opt-level = 0 @@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 37bf5b5daf..9067156125 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -287,7 +287,7 @@ impl ChatCloudService for AICloudServiceMiddleware { Some(model) => model.is_local, }; - info!("stream_complete use model: {:?}", ai_model); + info!("stream_complete use custom model: {:?}", ai_model); if use_local_ai { if self.local_ai.is_running() { match self From 7456c65799d31bb5b7595b9d6bd212075a09419f Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 27 Mar 2025 20:54:48 +0800 Subject: [PATCH 238/384] chore: clippy --- frontend/rust-lib/collab-integrate/src/collab_builder.rs | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 8 ++++---- .../rust-lib/flowy-core/src/deps_resolve/collab_deps.rs | 2 +- .../flowy-core/src/deps_resolve/folder_deps/mod.rs | 2 +- frontend/rust-lib/flowy-database2/src/manager.rs | 6 +++--- .../rust-lib/flowy-server/src/af_cloud/impls/database.rs | 4 ++-- .../src/services/data_import/appflowy_data_import.rs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index b6e89a5a2d..c94660dbfd 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -284,7 +284,7 @@ impl AppFlowyCollabBuilder { object.uid, object.workspace_id.clone(), object.object_id.to_string(), - object.collab_type.clone(), + object.collab_type, collab_db, persistence_config, ); diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 6092839747..000955cb33 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -390,7 +390,7 @@ impl AIManager { .get_server_available_models() .await? .into_iter() - .map(|m| AIModel::from(m)) + .map(AIModel::from) .collect(); trace!("[Model Selection]: Available models: {:?}", models); @@ -428,7 +428,7 @@ impl AIManager { None => { // when there is selected model and current local ai is active, then use local ai if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { - user_selected_model = AIModel::from(local_ai_model.clone()); + user_selected_model = local_ai_model.clone(); } }, Some(model) => { @@ -442,14 +442,14 @@ impl AIManager { .iter() .find(|m| m.name == user_selected_model.name) .cloned() - .or_else(|| Some(AIModel::from(server_active_model))); + .or(Some(server_active_model)); // Update the stored preference if a different model is used. if let Some(ref active_model) = active_model { if active_model.name != user_selected_model.name { self .store_preferences - .set_object::(&source_key, &AIModel::from(active_model.clone()))?; + .set_object::(&source_key, &active_model.clone())?; } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index a8827e06b0..c3d0358fcb 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -24,7 +24,7 @@ impl SnapshotPersistence for SnapshotDBImpl { collab_type: &CollabType, encoded_v1: Vec, ) -> Result<(), PersistenceError> { - let collab_type = collab_type.clone(); + let collab_type = *collab_type; let object_id = object_id.to_string(); let weak_user = self.0.clone(); tokio::task::spawn_blocking(move || { diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs index 9bed61d918..ef69fe7d82 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs @@ -197,7 +197,7 @@ impl FolderQueryService for FolderServiceImpl { } async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option { - let encode_collab = get_encoded_collab_v1_from_disk(&self.user, object_id, collab_type.clone()) + let encode_collab = get_encoded_collab_v1_from_disk(&self.user, object_id, collab_type) .await .ok(); diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index fca8db4f97..63f95011bd 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -776,7 +776,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { collab_type: CollabType, encoded_collab: Option<(EncodedCollab, bool)>, ) -> Result { - let object = self.build_collab_object(object_id, collab_type.clone())?; + let object = self.build_collab_object(object_id, collab_type)?; let data_source = if self.persistence.is_collab_exist(object_id) { trace!( "build collab: {}:{} from local encode collab", @@ -796,7 +796,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { object_id, encoded_collab.is_none(), ); - match self.get_encode_collab(object_id, collab_type.clone()).await { + match self.get_encode_collab(object_id, collab_type).await { Ok(Some(encode_collab)) => { info!( "build collab: {}:{} with remote encode collab, {} bytes", @@ -885,7 +885,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { .filter_map(|object_id| { self .persistence - .get_encoded_collab(object_id.as_str(), collab_type.clone()) + .get_encoded_collab(object_id.as_str(), collab_type) .map(|encoded_collab| (object_id.clone(), encoded_collab)) }) .collect(); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 4d264365ec..51e01bf4c1 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -45,7 +45,7 @@ where let cloned_user = self.user.clone(); let params = QueryCollabParams { workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id.clone(), collab_type.clone()), + inner: QueryCollab::new(object_id.clone(), collab_type), }; let result = try_get_client?.get_collab(params).await; match result { @@ -102,7 +102,7 @@ where let client = try_get_client?; let params = object_ids .into_iter() - .map(|object_id| QueryCollab::new(object_id, object_ty.clone())) + .map(|object_id| QueryCollab::new(object_id, object_ty)) .collect(); let results = client.batch_get_collab(&workspace_id, params).await?; check_request_workspace_id_is_match(&workspace_id, &cloned_user, "batch get database object")?; diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 129b605281..c2325815d6 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -1248,7 +1248,7 @@ pub async fn upload_collab_objects_data( objects.push(UserCollabParams { object_id: oid, encoded_collab, - collab_type: collab_type.clone(), + collab_type, }); size_counter += obj_size; } From 07c767c4fadbc8834649cf559a65561921e88095 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:23:29 +0800 Subject: [PATCH 239/384] fix: ai writer ux issues (#7640) * chore: fix local ai font weight * chore: adjust auto response format copy * fix: escape shortcut * chore: more action button icon size --- .../desktop_prompt_text_field.dart | 37 +++--- .../ai/ai_writer_block_component.dart | 13 ++- .../ai_writer_prompt_input_more_button.dart | 2 +- .../ai/widgets/ai_writer_scroll_wrapper.dart | 110 ++++++++++-------- .../setting_ai_view/local_ai_setting.dart | 2 +- frontend/resources/translations/en.json | 2 +- 6 files changed, 94 insertions(+), 72 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index fcf487da94..97850f6a1c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -55,16 +55,18 @@ class _DesktopPromptInputState extends State { super.initState(); widget.textController.addListener(handleTextControllerChanged); - focusNode.addListener( - () { - if (!widget.hideDecoration) { - setState(() {}); // refresh border color - } - if (!focusNode.hasFocus) { - cancelMentionPage(); // hide menu when lost focus - } - }, - ); + focusNode + ..addListener( + () { + if (!widget.hideDecoration) { + setState(() {}); // refresh border color + } + if (!focusNode.hasFocus) { + cancelMentionPage(); // hide menu when lost focus + } + }, + ) + ..onKeyEvent = handleKeyEvent; updateSendButtonState(); @@ -344,11 +346,16 @@ class _DesktopPromptInputState extends State { } KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - if (event.character == '@') { - WidgetsBinding.instance.addPostFrameCallback((_) { - inputControlCubit.startSearching(widget.textController.value); - overlayController.show(); - }); + // if (event.character == '@') { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // inputControlCubit.startSearching(widget.textController.value); + // overlayController.show(); + // }); + // } + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + node.unfocus(); + return KeyEventResult.handled; } return KeyEventResult.ignored; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 0f1e077d6d..769fabcd1f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -98,6 +98,7 @@ class _AIWriterBlockComponentState extends State { final textController = TextEditingController(); final overlayController = OverlayPortalController(); final layerLink = LayerLink(); + final focusNode = FocusNode(); late final editorState = context.read(); @@ -114,6 +115,7 @@ class _AIWriterBlockComponentState extends State { @override void dispose() { textController.dispose(); + focusNode.dispose(); super.dispose(); } @@ -145,10 +147,13 @@ class _AIWriterBlockComponentState extends State { bottom: 16.0, ), width: constraints.maxWidth, - child: OverlayContent( - editorState: editorState, - node: widget.node, - textController: textController, + child: Focus( + focusNode: focusNode, + child: OverlayContent( + editorState: editorState, + node: widget.node, + textController: textController, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart index 75c6589326..72b8d9560b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart @@ -135,9 +135,9 @@ class MoreAiWriterCommands extends StatelessWidget { return FlowyButton( leftIcon: FlowySvg( command.icon, - size: const Size.square(16), color: Theme.of(context).iconTheme.color, ), + leftIconSize: const Size.square(20), margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), text: FlowyText( command.i18n, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 326d1a44ab..90bce596a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -36,11 +36,9 @@ class _AiWriterScrollWrapperState extends State { onCreateNode: () { aiWriterRegistered = true; widget.editorState.service.keyboardService?.disableShortcuts(); - HardwareKeyboard.instance.addHandler(cancelShortcutHandler); }, onRemoveNode: () { aiWriterRegistered = false; - HardwareKeyboard.instance.removeHandler(cancelShortcutHandler); widget.editorState.service.keyboardService?.enableShortcuts(); }, onAppendToDocument: onAppendToDocument, @@ -48,6 +46,7 @@ class _AiWriterScrollWrapperState extends State { bool userHasScrolled = false; bool aiWriterRegistered = false; + bool dialogShown = false; @override void initState() { @@ -67,46 +66,51 @@ class _AiWriterScrollWrapperState extends State { value: aiWriterCubit, child: NotificationListener( onNotification: handleScrollNotification, - child: MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) { - if (state is DocumentContentEmptyAiWriterState) { - showConfirmDialog( - context: context, - title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: LocaleKeys - .ai_continueWritingEmptyDocumentDescription - .tr(), - onConfirm: state.onConfirm, - ); - } - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous is GeneratingAiWriterState && - current is ReadyAiWriterState, - listener: (context, state) { - widget.editorState.updateSelectionWithReason(null); - }, - ), - ], - child: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return BlocBuilder( - builder: (context, state) { - return AiWriterGestureDetector( - behavior: state is RegisteredAiWriter - ? HitTestBehavior.translucent - : HitTestBehavior.deferToChild, - onPointerEvent: () => onTapOutside(context), - ); + child: Focus( + autofocus: true, + onKeyEvent: handleKeyEvent, + child: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state is DocumentContentEmptyAiWriterState) { + showConfirmDialog( + context: context, + title: + LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: LocaleKeys + .ai_continueWritingEmptyDocumentDescription + .tr(), + onConfirm: state.onConfirm, + ); + } }, - ); - }, - child: widget.child, + ), + BlocListener( + listenWhen: (previous, current) => + previous is GeneratingAiWriterState && + current is ReadyAiWriterState, + listener: (context, state) { + widget.editorState.updateSelectionWithReason(null); + }, + ), + ], + child: OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return BlocBuilder( + builder: (context, state) { + return AiWriterGestureDetector( + behavior: state is RegisteredAiWriter + ? HitTestBehavior.translucent + : HitTestBehavior.deferToChild, + onPointerEvent: () => onTapOutside(context), + ); + }, + ); + }, + child: widget.child, + ), ), ), ), @@ -153,9 +157,15 @@ class _AiWriterScrollWrapperState extends State { } } - bool cancelShortcutHandler(KeyEvent event) { - if (event is! KeyUpEvent) { - return false; + KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { + if (!aiWriterRegistered) { + return KeyEventResult.ignored; + } + if (dialogShown) { + return KeyEventResult.handled; + } + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; } switch (event.logicalKey) { @@ -163,6 +173,7 @@ class _AiWriterScrollWrapperState extends State { if (aiWriterCubit.state case GeneratingAiWriterState _) { aiWriterCubit.stopStream(); } else if (aiWriterCubit.hasUnusedResponse()) { + dialogShown = true; showConfirmDialog( context: context, title: LocaleKeys.button_discard.tr(), @@ -171,23 +182,22 @@ class _AiWriterScrollWrapperState extends State { style: ConfirmPopupStyle.cancelAndOk, onConfirm: stopAndExit, onCancel: () {}, - ); + ).then((_) => dialogShown = false); } else { stopAndExit(); } - return true; + return KeyEventResult.handled; case LogicalKeyboardKey.keyC - when HardwareKeyboard.instance.logicalKeysPressed - .contains(LogicalKeyboardKey.controlLeft): + when HardwareKeyboard.instance.isControlPressed: if (aiWriterCubit.state case GeneratingAiWriterState _) { aiWriterCubit.stopStream(); } - return true; + return KeyEventResult.handled; default: break; } - return false; + return KeyEventResult.ignored; } void onAppendToDocument() { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index feec20afdb..57a72b6ca1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -84,7 +84,7 @@ class LocalAISettingHeader extends StatelessWidget { children: [ Row( children: [ - FlowyText( + FlowyText.medium( LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), ), const Spacer(), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 429f1ea8c3..2b22da596c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -246,7 +246,7 @@ "number": "Numbered list", "table": "Table", "blankDescription": "Format response", - "defaultDescription": "Auto mode", + "defaultDescription": "Auto response format", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", From 8aa32ca3fac69867c6da11124542a26157a4f86c Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 28 Mar 2025 16:27:16 +0800 Subject: [PATCH 240/384] chore: disable mcp --- frontend/rust-lib/Cargo.lock | 6 +++--- frontend/rust-lib/Cargo.toml | 6 +++--- frontend/rust-lib/flowy-ai/src/lib.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 07b39ecaa0..3119cec96c 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=bcd9782fa3d6f6d36f2fa6d065e834a1400f156e#bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 02df770eb1..7aa9023d5e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "bcd9782fa3d6f6d36f2fa6d065e834a1400f156e" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 3a15eec576..6ab100fd6e 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -7,8 +7,8 @@ mod completion; pub mod entities; mod local_ai; -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub mod mcp; +// #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +// pub mod mcp; mod middleware; pub mod notification; From fbf031b06d9461b87c2fa770f97e1f3fffb4560c Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 28 Mar 2025 23:00:27 +0800 Subject: [PATCH 241/384] chore: bump local ai --- frontend/rust-lib/Cargo.lock | 6 +++--- frontend/rust-lib/Cargo.toml | 6 +++--- .../rust-lib/flowy-ai/src/local_ai/controller.rs | 13 +++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 0e996ae1ff..d59db1a875 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=94913c8fb922047f52c6d01d0e16413ce25c970f#94913c8fb922047f52c6d01d0e16413ce25c970f" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index bbae7bc0f3..66a82bc2ca 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "94913c8fb922047f52c6d01d0e16413ce25c970f" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 59113a59ae..79a599b904 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -98,16 +98,17 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - - let new_state = RunningStatePB::from(state); - let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); let mut ready = false; let mut lack_of_resource = None; - if enabled { - ready = is_plugin_ready(); - lack_of_resource = cloned_llm_res.get_lack_of_resource().await; + let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + if !matches!(state, RunningState::UnexpectedStop { .. }) { + if enabled { + ready = is_plugin_ready(); + lack_of_resource = cloned_llm_res.get_lack_of_resource().await; + } } + let new_state = RunningStatePB::from(state); chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::UpdateLocalAIState, From 7b89d76ceac05c811636e49291bd5b3d1b009142 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 28 Mar 2025 23:31:00 +0800 Subject: [PATCH 242/384] chore: clippy --- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 79a599b904..c9b9ab52be 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -101,11 +101,9 @@ impl LocalAIController { let mut ready = false; let mut lack_of_resource = None; let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); - if !matches!(state, RunningState::UnexpectedStop { .. }) { - if enabled { - ready = is_plugin_ready(); - lack_of_resource = cloned_llm_res.get_lack_of_resource().await; - } + if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { + ready = is_plugin_ready(); + lack_of_resource = cloned_llm_res.get_lack_of_resource().await; } let new_state = RunningStatePB::from(state); From 917aa60c9881e2d987990cf9ad641a7969e2bfd8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 29 Mar 2025 15:32:45 +0800 Subject: [PATCH 243/384] chore: bump lai --- frontend/rust-lib/Cargo.lock | 6 +++--- frontend/rust-lib/Cargo.toml | 6 +++--- frontend/rust-lib/flowy-ai/Cargo.toml | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 12 +++++++++++- .../flowy-ai/src/local_ai/controller.rs | 17 ++++++++--------- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index d59db1a875..01b9251e23 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e#dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 66a82bc2ca..b4a0a34b14 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "dd7002f75a981a3ce0d5456eb7dac9ca0bb88a5e" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 22cecd3713..ee056021c4 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -35,7 +35,7 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -af-local-ai = { version = "0.1.0", features = ["verbose"] } +af-local-ai = { version = "0.1.0" } af-plugin = { version = "0.1.0" } reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 10f7bc2d09..eabf41a1e3 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -117,8 +117,18 @@ impl AIManager { } } + #[instrument(skip_all, err)] pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { - self.local_ai.reload().await?; + let local_ai = self.local_ai.clone(); + tokio::spawn(async move { + if let Err(err) = local_ai.destroy_plugin().await { + error!("Failed to destroy plugin: {}", err); + } + + if let Err(err) = local_ai.reload().await { + error!("[AI Manager] failed to reload local AI: {:?}", err); + } + }); Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c9b9ab52be..c3f57e6fac 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -175,7 +175,6 @@ impl LocalAIController { pub async fn reload(&self) -> FlowyResult<()> { let is_enabled = self.is_enabled(); - self.toggle_plugin(is_enabled).await?; Ok(()) } @@ -459,7 +458,6 @@ async fn initialize_ai_plugin( llm_resource: &Arc, ret: Option>, ) -> FlowyResult<()> { - let plugin = plugin.clone(); let lack_of_resource = llm_resource.get_lack_of_resource().await; chat_notification_builder( @@ -489,16 +487,17 @@ async fn initialize_ai_plugin( }) .send(); - if let Err(err) = plugin.destroy_plugin().await { - error!( - "[AI Plugin] failed to destroy plugin when lack of resource: {:?}", - err - ); - } - return Ok(()); } + if let Err(err) = plugin.destroy_plugin().await { + error!( + "[AI Plugin] failed to destroy plugin when lack of resource: {:?}", + err + ); + } + + let plugin = plugin.clone(); let cloned_llm_res = llm_resource.clone(); tokio::task::spawn_blocking(move || { futures::executor::block_on(async move { From 3a879b01865c0823acbf83d0bee0ec5c4c457baa Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 29 Mar 2025 21:56:05 +0800 Subject: [PATCH 244/384] feat: improve local ai settings page UI (#7643) * chore: improve local ai settings page * chore: improve local ai settings page * chore: simplify state and improve ui --- .../ai/download_offline_ai_app_bloc.dart | 41 --- .../settings/ai/local_ai_bloc.dart | 147 +++++---- .../ai/local_ai_on_boarding_bloc.dart | 2 +- .../ai/local_ai_setting_panel_bloc.dart | 92 ------ .../settings/ai/ollama_setting_bloc.dart | 8 +- .../settings/ai/plugin_state_bloc.dart | 146 --------- .../pages/setting_ai_view/init_local_ai.dart | 82 ----- .../setting_ai_view/local_ai_setting.dart | 244 ++++++++------ .../local_ai_setting_panel.dart | 32 -- .../pages/setting_ai_view/ollama_setting.dart | 114 +++++++ .../pages/setting_ai_view/ollma_setting.dart | 168 ---------- .../pages/setting_ai_view/plugin_state.dart | 248 -------------- .../plugin_status_indicator.dart | 309 ++++++++++++++++++ frontend/resources/translations/en.json | 14 +- frontend/rust-lib/flowy-ai/src/entities.rs | 34 +- .../flowy-ai/src/local_ai/controller.rs | 23 +- .../flowy-ai/src/local_ai/resource.rs | 62 ++-- 17 files changed, 731 insertions(+), 1035 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart deleted file mode 100644 index 185a8c049f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl; -part 'download_offline_ai_app_bloc.freezed.dart'; - -class DownloadOfflineAIBloc - extends Bloc { - DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) { - on(_handleEvent); - } - - Future _handleEvent( - DownloadOfflineAIEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIDownloadLink().send(); - await result.fold( - (app) async { - await launchUrl(Uri.parse(app.link)); - }, - (err) {}, - ); - }, - ); - } -} - -@freezed -class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent { - const factory DownloadOfflineAIEvent.started() = _Started; -} - -@freezed -class DownloadOfflineAIState with _$DownloadOfflineAIState { - const factory DownloadOfflineAIState() = _DownloadOfflineAIState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 66ba7fe572..24befd9480 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -1,94 +1,121 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'local_llm_listener.dart'; + part 'local_ai_bloc.freezed.dart'; -class LocalAIToggleBloc extends Bloc { - LocalAIToggleBloc() : super(const LocalAIToggleState()) { - on(_handleEvent); +class LocalAiPluginBloc extends Bloc { + LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) { + on(_handleEvent); + _startListening(); + _getLocalAiState(); + } + + final listener = LocalAIStateListener(); + + @override + Future close() async { + await listener.stop(); + return super.close(); } Future _handleEvent( - LocalAIToggleEvent event, - Emitter emit, + LocalAiPluginEvent event, + Emitter emit, ) async { await event.when( - started: () async { - final result = await AIEventGetLocalAIState().send(); - _handleResult(emit, result); + didReceiveAiState: (aiState) { + emit( + ReadyLocalAiPluginState( + isEnabled: aiState.enabled, + runningState: aiState.state, + lackOfResource: + aiState.hasLackOfResource() ? aiState.lackOfResource : null, + ), + ); + }, + didReceiveLackOfResources: (resources) { + if (state case final ReadyLocalAiPluginState readyState) { + emit( + ReadyLocalAiPluginState( + isEnabled: readyState.isEnabled, + runningState: readyState.runningState, + lackOfResource: resources, + ), + ); + } }, toggle: () async { - emit( - state.copyWith( - pageIndicator: const LocalAIToggleStateIndicator.loading(), - ), - ); - unawaited( - AIEventToggleLocalAI().send().then( - (result) { - if (!isClosed) { - add(LocalAIToggleEvent.handleResult(result)); - } - }, - ), + emit(LoadingLocalAiPluginState()); + await AIEventToggleLocalAI().send().fold( + (aiState) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + }, + Log.error, ); }, - handleResult: (result) { - _handleResult(emit, result); + restart: () async { + emit(LoadingLocalAiPluginState()); + await AIEventRestartLocalAI().send(); }, ); } - void _handleResult( - Emitter emit, - FlowyResult result, - ) { - result.fold( - (localAI) { - emit( - state.copyWith( - pageIndicator: - LocalAIToggleStateIndicator.isEnabled(localAI.enabled), - ), - ); + void _startListening() { + listener.start( + stateCallback: (pluginState) { + add(LocalAiPluginEvent.didReceiveAiState(pluginState)); }, - (err) { - emit( - state.copyWith( - pageIndicator: LocalAIToggleStateIndicator.error(err), - ), - ); + resourceCallback: (data) { + add(LocalAiPluginEvent.didReceiveLackOfResources(data)); }, ); } + + void _getLocalAiState() { + AIEventGetLocalAIState().send().fold( + (aiState) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + }, + Log.error, + ); + } } @freezed -class LocalAIToggleEvent with _$LocalAIToggleEvent { - const factory LocalAIToggleEvent.started() = _Started; - const factory LocalAIToggleEvent.toggle() = _Toggle; - const factory LocalAIToggleEvent.handleResult( - FlowyResult result, - ) = _HandleResult; +class LocalAiPluginEvent with _$LocalAiPluginEvent { + const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) = + _DidReceiveAiState; + const factory LocalAiPluginEvent.didReceiveLackOfResources( + LackOfAIResourcePB resources, + ) = _DidReceiveLackOfResources; + const factory LocalAiPluginEvent.toggle() = _Toggle; + const factory LocalAiPluginEvent.restart() = _Restart; } -@freezed -class LocalAIToggleState with _$LocalAIToggleState { - const factory LocalAIToggleState({ - @Default(LocalAIToggleStateIndicator.loading()) - LocalAIToggleStateIndicator pageIndicator, - }) = _LocalAIToggleState; +sealed class LocalAiPluginState { + const LocalAiPluginState(); } -@freezed -class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator { - // when start downloading the model - const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError; - const factory LocalAIToggleStateIndicator.isEnabled(bool isEnabled) = _Ready; - const factory LocalAIToggleStateIndicator.loading() = _Loading; +class ReadyLocalAiPluginState extends LocalAiPluginState { + const ReadyLocalAiPluginState({ + required this.isEnabled, + required this.runningState, + required this.lackOfResource, + }); + + final bool isEnabled; + final RunningStatePB runningState; + final LackOfAIResourcePB? lackOfResource; +} + +class LoadingLocalAiPluginState extends LocalAiPluginState { + const LoadingLocalAiPluginState(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 2c1bf34a87..3bb26a182b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -26,7 +26,7 @@ class LocalAIOnBoardingBloc _dispatch(); } - Future _onPaymentSuccessful() async { + void _onPaymentSuccessful() { if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart deleted file mode 100644 index f6d5ef949d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'local_ai_setting_panel_bloc.freezed.dart'; - -class LocalAISettingPanelBloc - extends Bloc { - LocalAISettingPanelBloc() - : listener = LocalAIStateListener(), - super(const LocalAISettingPanelState()) { - on(_handleEvent); - - listener.start( - stateCallback: (newState) { - if (!isClosed) { - add(LocalAISettingPanelEvent.updateAIState(newState)); - } - }, - ); - - AIEventGetLocalAIState().send().fold( - (localAIState) { - if (!isClosed) { - add(LocalAISettingPanelEvent.updateAIState(localAIState)); - } - }, - Log.error, - ); - } - - final LocalAIStateListener listener; - - /// Handles incoming events and dispatches them to the appropriate handler. - Future _handleEvent( - LocalAISettingPanelEvent event, - Emitter emit, - ) async { - event.when( - updateAIState: (LocalAIPB pluginState) { - if (pluginState.isPluginExecutableReady) { - emit( - state.copyWith( - runningState: pluginState.state, - progressIndicator: const LocalAIProgress.checkPluginState(), - ), - ); - } else { - emit( - state.copyWith( - progressIndicator: const LocalAIProgress.downloadLocalAIApp(), - ), - ); - } - }, - ); - } - - @override - Future close() async { - await listener.stop(); - return super.close(); - } -} - -@freezed -class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent { - const factory LocalAISettingPanelEvent.updateAIState( - LocalAIPB aiState, - ) = _UpdateAIState; -} - -@freezed -class LocalAISettingPanelState with _$LocalAISettingPanelState { - const factory LocalAISettingPanelState({ - LocalAIProgress? progressIndicator, - @Default(RunningStatePB.Connecting) RunningStatePB runningState, - }) = _LocalAIChatSettingState; -} - -@freezed -class LocalAIProgress with _$LocalAIProgress { - const factory LocalAIProgress.checkPluginState() = _CheckPluginStateProgress; - const factory LocalAIProgress.downloadLocalAIApp() = - _DownloadLocalAIAppProgress; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart index 8555d8cdc8..f5c4209028 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart @@ -80,11 +80,9 @@ class OllamaSettingBloc extends Bloc { } add(OllamaSettingEvent.updateSetting(setting)); AIEventUpdateLocalAISetting(setting).send().fold( - (_) { - Log.info('AI setting updated successfully'); - }, - (err) => Log.error("update ai setting failed: $err"), - ); + (_) => Log.info('AI setting updated successfully'), + (err) => Log.error("update ai setting failed: $err"), + ); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart deleted file mode 100644 index 4c3130ea00..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'plugin_state_bloc.freezed.dart'; - -class PluginStateBloc extends Bloc { - PluginStateBloc() - : listener = LocalAIStateListener(), - super( - const PluginStateState( - action: PluginStateAction.unknown(), - ), - ) { - listener.start( - stateCallback: (pluginState) { - if (!isClosed) { - add(PluginStateEvent.updateLocalAIState(pluginState)); - } - }, - resourceCallback: (data) { - if (!isClosed) { - add(PluginStateEvent.resourceStateChange(data)); - } - }, - ); - - on(_handleEvent); - } - - final LocalAIStateListener listener; - - @override - Future close() async { - await listener.stop(); - return super.close(); - } - - Future _handleEvent( - PluginStateEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIState().send(); - result.fold( - (pluginState) { - if (!isClosed) { - add(PluginStateEvent.updateLocalAIState(pluginState)); - } - }, - (err) => Log.error(err.toString()), - ); - }, - updateLocalAIState: (LocalAIPB aiState) { - // if the offline ai is not started, ask user to start it - if (aiState.hasLackOfResource()) { - emit( - PluginStateState( - action: PluginStateAction.lackOfResource(aiState.lackOfResource), - ), - ); - return; - } - - // Chech state of the plugin - switch (aiState.state) { - case RunningStatePB.ReadyToRun: - emit( - const PluginStateState( - action: PluginStateAction.readToRun(), - ), - ); - - case RunningStatePB.Connecting: - emit( - const PluginStateState( - action: PluginStateAction.initializingPlugin(), - ), - ); - case RunningStatePB.Connected: - emit( - const PluginStateState( - action: PluginStateAction.initializingPlugin(), - ), - ); - break; - case RunningStatePB.Running: - emit(const PluginStateState(action: PluginStateAction.running())); - break; - case RunningStatePB.Stopped: - emit( - state.copyWith(action: const PluginStateAction.restartPlugin()), - ); - default: - break; - } - }, - restartLocalAI: () async { - emit( - const PluginStateState(action: PluginStateAction.restartPlugin()), - ); - unawaited(AIEventRestartLocalAI().send()); - }, - resourceStateChange: (data) { - emit( - PluginStateState( - action: PluginStateAction.lackOfResource(data.resourceDesc), - ), - ); - }, - ); - } -} - -@freezed -class PluginStateEvent with _$PluginStateEvent { - const factory PluginStateEvent.started() = _Started; - const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) = - _UpdateLocalAIState; - const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; - const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) = - _ResourceStateChange; -} - -@freezed -class PluginStateState with _$PluginStateState { - const factory PluginStateState({ - required PluginStateAction action, - }) = _PluginStateState; -} - -@freezed -class PluginStateAction with _$PluginStateAction { - const factory PluginStateAction.unknown() = _Unknown; - const factory PluginStateAction.readToRun() = _ReadyToRun; - const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; - const factory PluginStateAction.running() = _PluginRunning; - const factory PluginStateAction.restartPlugin() = _RestartPlugin; - const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart deleted file mode 100644 index 2de410a5e5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class InitLocalAIIndicator extends StatelessWidget { - const InitLocalAIIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all( - Radius.circular(4), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: BlocBuilder( - builder: (context, state) { - switch (state.runningState) { - case RunningStatePB.Connecting: - case RunningStatePB.Connected: - return Row( - children: [ - const HSpace(8), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing - .tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ); - case RunningStatePB.Running: - return SizedBox( - height: 30, - child: Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ], - ), - ); - case RunningStatePB.Stopped: - return Row( - children: [ - const HSpace(8), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), - fontSize: 11, - color: const Color(0xFFC62828), - ), - ], - ); - default: - return const SizedBox.shrink(); - } - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 57a72b6ca1..080dc426d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -1,130 +1,174 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class LocalAISetting extends StatelessWidget { +import 'ollama_setting.dart'; +import 'plugin_status_indicator.dart'; + +class LocalAISetting extends StatefulWidget { const LocalAISetting({super.key}); + @override + State createState() => _LocalAISettingState(); +} + +class _LocalAISettingState extends State { + final expandableController = ExpandableController(initialExpanded: false); + + @override + void dispose() { + expandableController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ExpandableNotifier( - child: BlocListener( - listener: (context, state) { - final controller = - ExpandableController.of(context, required: true)!; - - state.pageIndicator.when( - error: (_) => controller.expanded = true, - isEnabled: (enabled) => controller.expanded = enabled, - loading: () => controller.expanded = true, - ); - }, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: const LocalAISettingHeader(), - collapsed: const SizedBox.shrink(), - expanded: Column( - children: [ - const VSpace(12), - DecoratedBox( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: LocalAISettingPanel(), - ), - ), - ], - ), + create: (context) => LocalAiPluginBloc(), + child: BlocConsumer( + listener: (context, state) { + final newIsExpanded = switch (state) { + final ReadyLocalAiPluginState readyLocalAiToggleState => + readyLocalAiToggleState.isEnabled, + _ => false, + }; + expandableController.value = newIsExpanded; + }, + builder: (context, state) { + return ExpandablePanel( + controller: expandableController, + theme: ExpandableThemeData( + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, ), - ), - ), + header: LocalAiSettingHeader( + isEnabled: state is ReadyLocalAiPluginState && state.isEnabled, + isToggleable: state is ReadyLocalAiPluginState, + ), + collapsed: const SizedBox.shrink(), + expanded: Padding( + padding: EdgeInsets.only(top: 12), + child: LocalAISettingPanel(), + ), + ); + }, ), ); } } -class LocalAISettingHeader extends StatelessWidget { - const LocalAISettingHeader({super.key}); +class LocalAiSettingHeader extends StatelessWidget { + const LocalAiSettingHeader({ + super.key, + required this.isEnabled, + required this.isToggleable, + }); + + final bool isEnabled; + final bool isToggleable; @override Widget build(BuildContext context) { - return BlocBuilder( + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + ), + const VSpace(4), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), + maxLines: 3, + fontSize: 12, + ), + ], + ), + ), + IgnorePointer( + ignoring: !isToggleable, + child: Opacity( + opacity: isToggleable ? 1 : 0.5, + child: Toggle( + value: isEnabled, + onChanged: (_) => _onToggleChanged(context), + ), + ), + ), + ], + ); + } + + void _onToggleChanged(BuildContext context) { + if (isEnabled) { + showConfirmDialog( + context: context, + title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), + description: + LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () { + context + .read() + .add(const LocalAiPluginEvent.toggle()); + }, + ); + } else { + context.read().add(const LocalAiPluginEvent.toggle()); + } + } +} + +class LocalAISettingPanel extends StatelessWidget { + const LocalAISettingPanel({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( builder: (context, state) { - return state.pageIndicator.when( - error: (error) => SizedBox.shrink(), - loading: () => const SizedBox.shrink(), - isEnabled: (isEnabled) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), - ), - const Spacer(), - Toggle( - value: isEnabled, - onChanged: (_) { - if (isEnabled) { - showConfirmDialog( - context: context, - title: LocaleKeys - .settings_aiPage_keys_disableLocalAITitle - .tr(), - description: LocaleKeys - .settings_aiPage_keys_disableLocalAIDescription - .tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () => context - .read() - .add(const LocalAIToggleEvent.toggle()), - ); - } else { - context - .read() - .add(const LocalAIToggleEvent.toggle()); - } - }, - ), - ], - ), - const VSpace(4), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), - maxLines: 3, - fontSize: 12, - ), - ], - ); - }, + if (state is! ReadyLocalAiPluginState) { + return const SizedBox.shrink(); + } + + final showIndicator = _showIndicator(state); + final showSettings = _showSettings(state); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showIndicator) const LocalAIStatusIndicator(), + if (showSettings && showIndicator) const VSpace(10), + if (showSettings) OllamaSettingPage(), + ], ); }, ); } + + bool _showIndicator(ReadyLocalAiPluginState state) { + return state.runningState != RunningStatePB.Running || + state.lackOfResource != null; + } + + bool _showSettings(ReadyLocalAiPluginState state) { + return ![ + RunningStatePB.Connecting, + RunningStatePB.Connected, + ].contains(state.runningState) && + (state.lackOfResource == null || + state.lackOfResource!.resourceType == + LackOfAIResourceTypePB.MissingModel); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart deleted file mode 100644 index 5357db5c91..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'plugin_state.dart'; - -class LocalAISettingPanel extends StatelessWidget { - const LocalAISettingPanel({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LocalAISettingPanelBloc(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OllamaSettingPage(), - VSpace(6), - PluginStateIndicator(), - ], - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart new file mode 100644 index 0000000000..2abcb552d1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OllamaSettingPage extends StatelessWidget { + const OllamaSettingPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + OllamaSettingBloc()..add(const OllamaSettingEvent.started()), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.inputItems != current.inputItems || + previous.isEdited != current.isEdited, + builder: (context, state) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + padding: EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10, + children: [ + for (final item in state.inputItems) + _SettingItemWidget(item: item), + _SaveButton(isEdited: state.isEdited), + ], + ), + ); + }, + ), + ); + } +} + +class _SettingItemWidget extends StatelessWidget { + const _SettingItemWidget({required this.item}); + + final SettingItem item; + + @override + Widget build(BuildContext context) { + return Column( + key: ValueKey(item.content + item.settingType.title), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + item.settingType.title, + fontSize: 12, + figmaLineHeight: 16, + ), + const VSpace(4), + SizedBox( + height: 32, + child: FlowyTextField( + hintText: item.hintText, + text: item.content, + onChanged: (content) { + context.read().add( + OllamaSettingEvent.onEdit(content, item.settingType), + ); + }, + ), + ), + ], + ); + } +} + +class _SaveButton extends StatelessWidget { + const _SaveButton({required this.isEdited}); + + final bool isEdited; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerEnd, + child: FlowyTooltip( + message: isEdited ? null : 'No changes', + child: SizedBox( + child: FlowyButton( + text: FlowyText( + 'Apply', + figmaLineHeight: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + disable: !isEdited, + expandText: false, + margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), + backgroundColor: Theme.of(context).colorScheme.primary, + hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200), + onTap: () { + if (isEdited) { + context + .read() + .add(const OllamaSettingEvent.submit()); + } + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart deleted file mode 100644 index 8af4e35914..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class OllamaSettingPage extends StatelessWidget { - const OllamaSettingPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - OllamaSettingBloc()..add(const OllamaSettingEvent.started()), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.inputItems != current.inputItems || - previous.isEdited != current.isEdited, - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.inputItems.length, - separatorBuilder: (_, __) => const VSpace(10), - itemBuilder: (context, index) { - final item = state.inputItems[index]; - return _SettingItemWidget(item: item); - }, - ), - const VSpace(6), - Opacity( - opacity: 0.6, - child: _InstallOllamaInstruction(), - ), - _SaveButton(isEdited: state.isEdited), - ], - ); - }, - ), - ); - } -} - -class _SettingItemWidget extends StatelessWidget { - const _SettingItemWidget({required this.item}); - final SettingItem item; - - @override - Widget build(BuildContext context) { - return Column( - key: ValueKey(item.content + item.settingType.title), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText(item.settingType.title), - const VSpace(8), - FlowyTextField( - hintText: item.hintText, - text: item.content, - onChanged: (content) { - context.read().add( - OllamaSettingEvent.onEdit(content, item.settingType), - ); - }, - ), - ], - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({required this.isEdited}); - - final bool isEdited; - - @override - Widget build(BuildContext context) { - final tooltipMessage = isEdited ? 'Click to apply changes' : 'No changes'; - return SizedBox( - height: 50, - child: Row( - children: [ - const Spacer(), - SizedBox( - width: 120, - child: FlowyTooltip( - message: tooltipMessage, - child: Opacity( - opacity: isEdited ? 1 : 0.5, - child: FlowyTextButton( - 'Apply', - mainAxisAlignment: MainAxisAlignment.center, - onPressed: isEdited - ? () { - context - .read() - .add(const OllamaSettingEvent.submit()); - } - : null, - ), - ), - ), - ), - ], - ), - ); - } -} - -class _InstallOllamaInstruction extends StatelessWidget { - const _InstallOllamaInstruction(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - maxLines: 3, - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - ], - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart deleted file mode 100644 index ab9303b429..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; -import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PluginStateIndicator extends StatelessWidget { - const PluginStateIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - PluginStateBloc()..add(const PluginStateEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return state.action.when( - unknown: () => const SizedBox.shrink(), - readToRun: () => const _PrepareRunning(), - initializingPlugin: () => const InitLocalAIIndicator(), - running: () => const _LocalAIRunning(), - restartPlugin: () => const _RestartPluginButton(), - lackOfResource: (desc) => _LackOfResource(desc: desc), - ); - }, - ), - ); - } -} - -class _PrepareRunning extends StatelessWidget { - const _PrepareRunning(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - maxLines: 3, - ), - ), - ], - ); - } -} - -class _RestartPluginButton extends StatelessWidget { - const _RestartPluginButton(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const FlowySvg( - FlowySvgs.download_warn_s, - color: Color(0xFFC62828), - ), - const HSpace(6), - FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()), - const Spacer(), - SizedBox( - height: 30, - child: FlowyButton( - useIntrinsicWidth: true, - text: - FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()), - onTap: () { - context.read().add( - const PluginStateEvent.restartLocalAI(), - ); - }, - ), - ), - ], - ); - } -} - -class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning(); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all( - Radius.circular(4), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - Flexible( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class OpenOrDownloadOfflineAIApp extends StatelessWidget { - const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key}); - - final VoidCallback onRetry; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DownloadOfflineAIBloc(), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - maxLines: 3, - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline", - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = - () => context.read().add( - const DownloadOfflineAIEvent.started(), - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - ], - ), - ), - ], - ); - }, - ), - ); - } -} - -class _LackOfResource extends StatelessWidget { - const _LackOfResource({required this.desc}); - - final String desc; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - FlowySvgs.toast_warning_filled_s, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(6), - Expanded( - child: FlowyText( - desc, - maxLines: 3, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart new file mode 100644 index 0000000000..b79e228a0c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart @@ -0,0 +1,309 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalAIStatusIndicator extends StatelessWidget { + const LocalAIStatusIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final readyState = switch (state) { + final ReadyLocalAiPluginState readyLocalAiToggleState => + readyLocalAiToggleState, + _ => null, + }; + if (readyState == null) { + return const SizedBox.shrink(); + } + + final lackOfResource = readyState.lackOfResource; + if (lackOfResource != null) { + return _LackOfResource(resource: lackOfResource); + } + + return switch (readyState.runningState) { + RunningStatePB.ReadyToRun => const _ReadyToRun(), + RunningStatePB.Connecting || + RunningStatePB.Connected => + _Initializing(), + RunningStatePB.Running => const _LocalAIRunning(), + RunningStatePB.Stopped => const _RestartPluginButton(), + _ => const SizedBox.shrink(), + }; + }, + ); + } +} + +class _ReadyToRun extends StatelessWidget { + const _ReadyToRun(); + + @override + Widget build(BuildContext context) { + return FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStart.tr(), + maxLines: 3, + ); + } +} + +class _Initializing extends StatelessWidget { + const _Initializing(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + padding: const EdgeInsets.all(8.0), + width: double.infinity, + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ); + } +} + +class _RestartPluginButton extends StatelessWidget { + const _RestartPluginButton(); + + @override + Widget build(BuildContext context) { + final textStyle = + Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).isLightMode + ? const Color(0x80FFE7EE) + : const Color(0x80591734), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.toast_warning_filled_s, + color: Color(0xFFC62828), + ), + const HSpace(8), + Expanded( + child: RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: + LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(), + style: textStyle?.copyWith( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + afLaunchUrlString( + "https://appflowy.com/guide/appflowy-local-ai-ollama", + ); + }, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _LocalAIRunning extends StatelessWidget { + const _LocalAIRunning(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ), + ], + ), + ); + } +} + +class _LackOfResource extends StatelessWidget { + const _LackOfResource({required this.resource}); + + final LackOfAIResourcePB resource; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).isLightMode + ? const Color(0x80FFE7EE) + : const Color(0x80591734), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + FlowySvg( + FlowySvgs.toast_error_filled_s, + size: const Size.square(20.0), + blendMode: null, + ), + const HSpace(8), + Expanded( + child: switch (resource.resourceType) { + LackOfAIResourceTypePB.PluginExecutableNotReady => + _buildNoLAI(context), + LackOfAIResourceTypePB.OllamaServerNotReady => + _buildNoOllama(context), + LackOfAIResourceTypePB.MissingModel => + _buildNoModel(context, resource.missingModelNames), + _ => const SizedBox.shrink(), + }, + ), + ], + ), + ); + } + + TextStyle? _textStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); + } + + Widget _buildNoLAI(BuildContext context) { + final textStyle = _textStyle(context); + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(), + style: textStyle, + ), + TextSpan(text: ' ', style: _textStyle(context)), + ..._downloadInstructions(textStyle), + ], + ), + ); + } + + Widget _buildNoOllama(BuildContext context) { + final textStyle = _textStyle(context); + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(), + style: textStyle, + ), + TextSpan(text: ' ', style: textStyle), + ..._downloadInstructions(textStyle), + ], + ), + ); + } + + Widget _buildNoModel(BuildContext context, List modelNames) { + final textStyle = _textStyle(context); + + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + ], + ), + ); + } + + List _downloadInstructions(TextStyle? textStyle) { + return [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr(), + style: textStyle?.copyWith( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + afLaunchUrlString( + "https://appflowy.com/guide/appflowy-local-ai-ollama", + ); + }, + ), + TextSpan(text: ' ', style: textStyle), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr(), + style: textStyle, + ), + ]; + } +} + +void onDownloadButtonPressed() { + AIEventGetLocalAIDownloadLink().send().fold( + (app) => afLaunchUri(Uri.parse(app.link)), + (err) {}, + ); +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2b22da596c..01c3a51b19 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -854,14 +854,14 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", + "localAIStart": "Local AI is starting. If it's slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", + "localAIInitializing": "Local AI is loading. This may take a few minutes depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", - "restartLocalAI": "Restart Local AI", + "restartLocalAI": "Restart", "disableLocalAITitle": "Disable local AI", "disableLocalAIDescription": "Do you want to disable local AI?", "localAIToggleTitle": "AppFlowy Local AI (LAI)", @@ -875,10 +875,12 @@ "activeOfflineAI": "Active", "downloadOfflineAI": "Download", "openModelDirectory": "Open folder", - "localAISetupInstruction1": "Follow these", + "localAISetupInstruction1": "Please follow these", "localAISetupInstruction2": "instructions", - "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI. Skip if you've already set it up", - "startLocalAI": "It may take a few seconds to start the local AI" + "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI.", + "laiNotReady": "The Local AI app was not installed correctly.", + "ollamaNotReady": "The Ollama server is not ready.", + "modelsMissing": "Cannot find the model. You can use the ollama pull command to install them." } }, "planPage": { diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index b24d0cb13e..a7915aae23 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -589,7 +589,7 @@ pub struct LocalAIPB { pub is_plugin_executable_ready: bool, #[pb(index = 3, one_of)] - pub lack_of_resource: Option, + pub lack_of_resource: Option, #[pb(index = 4)] pub state: RunningStatePB, @@ -724,5 +724,35 @@ impl From for LocalAISetting { #[derive(Default, ProtoBuf, Clone, Debug)] pub struct LackOfAIResourcePB { #[pb(index = 1)] - pub resource_desc: String, + pub resource_type: LackOfAIResourceTypePB, + + #[pb(index = 2)] + pub missing_model_names: Vec, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum)] +pub enum LackOfAIResourceTypePB { + #[default] + PluginExecutableNotReady = 0, + OllamaServerNotReady = 1, + MissingModel = 2, +} + +impl From for LackOfAIResourcePB { + fn from(value: PendingResource) -> Self { + match value { + PendingResource::PluginExecutableNotReady => Self { + resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady, + missing_model_names: vec![], + }, + PendingResource::OllamaServerNotReady => Self { + resource_type: LackOfAIResourceTypePB::OllamaServerNotReady, + missing_model_names: vec![], + }, + PendingResource::MissingModel(model_name) => Self { + resource_type: LackOfAIResourceTypePB::MissingModel, + missing_model_names: vec![model_name], + }, + } + } } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c3f57e6fac..bca710a907 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -1,5 +1,5 @@ use crate::ai_manager::AIUserService; -use crate::entities::{LackOfAIResourcePB, LocalAIPB, RunningStatePB}; +use crate::entities::{LocalAIPB, RunningStatePB}; use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, @@ -274,14 +274,15 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let mut is_plugin_executable_ready = false; - let mut state = RunningState::ReadyToConnect; - let mut lack_of_resource = None; - if enabled { - is_plugin_executable_ready = is_plugin_ready(); - state = self.ai_plugin.get_plugin_running_state(); - lack_of_resource = self.resource.get_lack_of_resource().await; - } + let (is_plugin_executable_ready, state, lack_of_resource) = if enabled { + ( + is_plugin_ready(), + self.ai_plugin.get_plugin_running_state(), + self.resource.get_lack_of_resource().await, + ) + } else { + (false, RunningState::ReadyToConnect, None) + }; let elapsed = start.elapsed(); debug!( "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", @@ -482,9 +483,7 @@ async fn initialize_ai_plugin( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB { - resource_desc: lack_of_resource, - }) + .payload(lack_of_resource) .send(); return Ok(()); diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 3eddcf2039..c40a9a9b88 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -47,22 +47,13 @@ pub enum WatchDiskEvent { Remove, } +#[derive(Debug, Clone)] pub enum PendingResource { PluginExecutableNotReady, OllamaServerNotReady, MissingModel(String), } -impl PendingResource { - pub fn desc(self) -> String { - match self { - PendingResource::PluginExecutableNotReady => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), - PendingResource::OllamaServerNotReady => "Ollama is not ready. Please follow the instructions to install Ollama".to_string(), - PendingResource::MissingModel(model) => format!("Cannot find the model: {}. Please use the ollama pull command to install the model", model), - } - } -} - pub struct LocalAIResourceController { user_service: Arc, resource_service: Arc, @@ -128,10 +119,10 @@ impl LocalAIResourceController { return false; } - match self.calculate_pending_resources().await { - Ok(res) => res.is_empty(), - Err(_) => false, - } + self + .calculate_pending_resources() + .await + .is_ok_and(|r| r.is_none()) } pub async fn get_plugin_download_link(&self) -> FlowyResult { @@ -147,36 +138,35 @@ impl LocalAIResourceController { #[instrument(level = "info", skip_all, err)] pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { self.resource_service.store_setting(setting)?; - let mut resources = self.calculate_pending_resources().await?; - if let Some(resource) = resources.pop() { + if let Some(resource) = self.calculate_pending_resources().await? { chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB { - resource_desc: resource.desc(), - }) + .payload(LackOfAIResourcePB::from(resource)) .send(); } Ok(()) } - pub async fn get_lack_of_resource(&self) -> Option { - let mut resources = self.calculate_pending_resources().await.ok()?; - resources.pop().map(|r| r.desc()) + pub async fn get_lack_of_resource(&self) -> Option { + self + .calculate_pending_resources() + .await + .ok()? + .map(Into::into) } - pub async fn calculate_pending_resources(&self) -> FlowyResult> { - let mut resources = vec![]; + pub async fn calculate_pending_resources(&self) -> FlowyResult> { let app_path = ollama_plugin_path(); if !is_plugin_ready() { trace!("[LLM Resource] offline app not found: {:?}", app_path); - resources.push(PendingResource::PluginExecutableNotReady); - return Ok(resources); + return Ok(Some(PendingResource::PluginExecutableNotReady)); } let setting = self.get_llm_setting(); let client = Client::builder().timeout(Duration::from_secs(5)).build()?; + match client.get(&setting.ollama_server_url).send().await { Ok(resp) if resp.status().is_success() => { info!( @@ -189,8 +179,7 @@ impl LocalAIResourceController { "[LLM Resource] Ollama server is not responding at {}", setting.ollama_server_url ); - resources.push(PendingResource::OllamaServerNotReady); - return Ok(resources); + return Ok(Some(PendingResource::OllamaServerNotReady)); }, } @@ -201,12 +190,8 @@ impl LocalAIResourceController { match client.get(&tags_url).send().await { Ok(resp) if resp.status().is_success() => { - let tags: TagsResponse = resp.json().await.map_err(|e| { - log::error!( - "[LLM Resource] Failed to parse /api/tags JSON response: {:?}", - e - ); - e + let tags: TagsResponse = resp.json().await.inspect_err(|e| { + log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") })?; // Check each required model is present in the response. for required in &required_models { @@ -215,9 +200,7 @@ impl LocalAIResourceController { "[LLM Resource] required model '{}' not found in API response", required ); - resources.push(PendingResource::MissingModel(required.clone())); - // Optionally, you could continue checking all models rather than returning early. - return Ok(resources); + return Ok(Some(PendingResource::MissingModel(required.clone()))); } } }, @@ -226,12 +209,11 @@ impl LocalAIResourceController { "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", setting.ollama_server_url ); - resources.push(PendingResource::OllamaServerNotReady); - return Ok(resources); + return Ok(Some(PendingResource::OllamaServerNotReady)); }, } - Ok(resources) + Ok(None) } #[instrument(level = "info", skip_all)] From 34a858e94836466447f717cc2f0cd142ce34bbb7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 09:02:06 +0800 Subject: [PATCH 245/384] chore: show plugin version --- .../ai/download_offline_ai_app_bloc.dart | 41 ------- .../ai/local_ai_setting_panel_bloc.dart | 2 +- .../settings/ai/plugin_state_bloc.dart | 8 +- .../pages/setting_ai_view/plugin_state.dart | 105 ++---------------- frontend/resources/translations/ar-SA.json | 4 +- frontend/resources/translations/en.json | 4 +- frontend/resources/translations/ko-KR.json | 4 +- frontend/rust-lib/Cargo.lock | 6 +- frontend/rust-lib/Cargo.toml | 6 +- frontend/rust-lib/flowy-ai/src/entities.rs | 17 ++- .../rust-lib/flowy-ai/src/event_handler.rs | 9 -- frontend/rust-lib/flowy-ai/src/event_map.rs | 4 - .../flowy-ai/src/local_ai/controller.rs | 22 ++-- 13 files changed, 49 insertions(+), 183 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart deleted file mode 100644 index 185a8c049f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl; -part 'download_offline_ai_app_bloc.freezed.dart'; - -class DownloadOfflineAIBloc - extends Bloc { - DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) { - on(_handleEvent); - } - - Future _handleEvent( - DownloadOfflineAIEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIDownloadLink().send(); - await result.fold( - (app) async { - await launchUrl(Uri.parse(app.link)); - }, - (err) {}, - ); - }, - ); - } -} - -@freezed -class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent { - const factory DownloadOfflineAIEvent.started() = _Started; -} - -@freezed -class DownloadOfflineAIState with _$DownloadOfflineAIState { - const factory DownloadOfflineAIState() = _DownloadOfflineAIState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart index f6d5ef949d..60c68b70c6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart @@ -44,7 +44,7 @@ class LocalAISettingPanelBloc ) async { event.when( updateAIState: (LocalAIPB pluginState) { - if (pluginState.isPluginExecutableReady) { + if (pluginState.pluginDownloaded) { emit( state.copyWith( runningState: pluginState.state, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart index 4c3130ea00..d91d7151ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -91,7 +91,11 @@ class PluginStateBloc extends Bloc { ); break; case RunningStatePB.Running: - emit(const PluginStateState(action: PluginStateAction.running())); + emit( + PluginStateState( + action: PluginStateAction.running(aiState.pluginVersion), + ), + ); break; case RunningStatePB.Stopped: emit( @@ -140,7 +144,7 @@ class PluginStateAction with _$PluginStateAction { const factory PluginStateAction.unknown() = _Unknown; const factory PluginStateAction.readToRun() = _ReadyToRun; const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; - const factory PluginStateAction.running() = _PluginRunning; + const factory PluginStateAction.running(String version) = _PluginRunning; const factory PluginStateAction.restartPlugin() = _RestartPlugin; const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index ab9303b429..b8461eb141 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -1,15 +1,11 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,7 +23,7 @@ class PluginStateIndicator extends StatelessWidget { unknown: () => const SizedBox.shrink(), readToRun: () => const _PrepareRunning(), initializingPlugin: () => const InitLocalAIIndicator(), - running: () => const _LocalAIRunning(), + running: (version) => _LocalAIRunning(version: version), restartPlugin: () => const _RestartPluginButton(), lackOfResource: (desc) => _LackOfResource(desc: desc), ); @@ -88,7 +84,9 @@ class _RestartPluginButton extends StatelessWidget { } class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning(); + const _LocalAIRunning({required this.version}); + + final String version; @override Widget build(BuildContext context) { @@ -115,7 +113,11 @@ class _LocalAIRunning extends StatelessWidget { const HSpace(6), Flexible( child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), + LocaleKeys.settings_aiPage_keys_localAIRunning.tr( + args: [ + version, + ], + ), fontSize: 11, color: const Color(0xFF1E4620), maxLines: 3, @@ -131,95 +133,6 @@ class _LocalAIRunning extends StatelessWidget { } } -class OpenOrDownloadOfflineAIApp extends StatelessWidget { - const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key}); - - final VoidCallback onRetry; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DownloadOfflineAIBloc(), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - maxLines: 3, - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline", - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = - () => context.read().add( - const DownloadOfflineAIEvent.started(), - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - ], - ), - ), - ], - ); - }, - ), - ); - } -} - class _LackOfResource extends StatelessWidget { const _LackOfResource({required this.desc}); diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 62708e664f..bb4534d855 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -857,7 +857,7 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", + "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل. الإصدار: {}", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", @@ -3217,4 +3217,4 @@ "rewrite": "إعادة كتابة", "insertBelow": "أدخل أدناه" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2b22da596c..8129157941 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -857,7 +857,7 @@ "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running", + "localAIRunning": "Local AI is running. Version: {}", "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", @@ -3202,4 +3202,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index f956327073..7ed8373c72 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -851,7 +851,7 @@ "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다", + "localAIRunning": "로컬 AI가 실행 중입니다. 버전: {}", "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", @@ -3185,4 +3185,4 @@ "rewrite": "다시 작성", "insertBelow": "아래에 삽입" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 01b9251e23..6bc02bda8a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b4a0a34b14..6baa7dea3e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index b24d0cb13e..30a3e5dd28 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -585,20 +585,17 @@ pub struct LocalAIPB { #[pb(index = 1)] pub enabled: bool, - #[pb(index = 2)] - pub is_plugin_executable_ready: bool, - - #[pb(index = 3, one_of)] + #[pb(index = 2, one_of)] pub lack_of_resource: Option, - #[pb(index = 4)] + #[pb(index = 3)] pub state: RunningStatePB, -} -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalAIAppLinkPB { - #[pb(index = 1)] - pub link: String, + #[pb(index = 4, one_of)] + pub plugin_version: Option, + + #[pb(index = 5)] + pub plugin_downloaded: bool, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index ec8b7b4964..3150d3e378 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -301,15 +301,6 @@ pub(crate) async fn get_local_ai_state_handler( data_result_ok(state) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_offline_app_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let link = ai_manager.local_ai.get_plugin_download_link().await?; - data_result_ok(LocalAIAppLinkPB { link }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn create_chat_context_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 0e24ca6a21..51c49eaabb 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -34,7 +34,6 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::RestartLocalAI, restart_local_ai_handler) .event(AIEvent::ToggleLocalAI, toggle_local_ai_handler) .event(AIEvent::GetLocalAIState, get_local_ai_state_handler) - .event(AIEvent::GetLocalAIDownloadLink, get_offline_app_handler) .event(AIEvent::GetLocalAISetting, get_local_ai_setting_handler) .event( AIEvent::UpdateLocalAISetting, @@ -97,9 +96,6 @@ pub enum AIEvent { #[event(output = "LocalAIPB")] GetLocalAIState = 19, - #[event(output = "LocalAIAppLinkPB")] - GetLocalAIDownloadLink = 22, - #[event(input = "CreateChatContextPB")] CreateChatContext = 23, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c3f57e6fac..1bbe356e2b 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -98,11 +98,11 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - let mut ready = false; + let mut plugin_downloaded = false; let mut lack_of_resource = None; let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - ready = is_plugin_ready(); + plugin_downloaded = is_plugin_ready(); lack_of_resource = cloned_llm_res.get_lack_of_resource().await; } @@ -113,9 +113,10 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_plugin_executable_ready: ready, + plugin_downloaded, lack_of_resource, state: new_state, + plugin_version: None, }) .send(); } @@ -274,12 +275,14 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let mut is_plugin_executable_ready = false; + let mut plugin_downloaded = false; let mut state = RunningState::ReadyToConnect; let mut lack_of_resource = None; + let mut plugin_version = None; if enabled { - is_plugin_executable_ready = is_plugin_ready(); + plugin_downloaded = is_plugin_ready(); state = self.ai_plugin.get_plugin_running_state(); + plugin_version = self.ai_plugin.plugin_info().await.ok().map(|v| v.version); lack_of_resource = self.resource.get_lack_of_resource().await; } let elapsed = start.elapsed(); @@ -290,9 +293,10 @@ impl LocalAIController { ); LocalAIPB { enabled, - is_plugin_executable_ready, + plugin_downloaded, state: RunningStatePB::from(state), lack_of_resource, + plugin_version, } } @@ -442,9 +446,10 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_plugin_executable_ready: true, + plugin_downloaded: true, state: RunningStatePB::Stopped, lack_of_resource: None, + plugin_version: None, }) .send(); } @@ -466,9 +471,10 @@ async fn initialize_ai_plugin( ) .payload(LocalAIPB { enabled: true, - is_plugin_executable_ready: true, + plugin_downloaded: true, state: RunningStatePB::ReadyToRun, lack_of_resource: lack_of_resource.clone(), + plugin_version: None, }) .send(); From 12a4bf67f64249620f6d25b132080a82bf901dc6 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 30 Mar 2025 09:54:08 +0800 Subject: [PATCH 246/384] Revert "feat: improve local ai settings page UI (#7643)" This reverts commit 3a879b01865c0823acbf83d0bee0ec5c4c457baa. --- .../ai/download_offline_ai_app_bloc.dart | 41 +++ .../settings/ai/local_ai_bloc.dart | 151 ++++----- .../ai/local_ai_on_boarding_bloc.dart | 2 +- .../ai/local_ai_setting_panel_bloc.dart | 92 ++++++ .../settings/ai/ollama_setting_bloc.dart | 8 +- .../settings/ai/plugin_state_bloc.dart | 146 +++++++++ .../pages/setting_ai_view/init_local_ai.dart | 82 +++++ .../setting_ai_view/local_ai_setting.dart | 244 ++++++-------- .../local_ai_setting_panel.dart | 32 ++ .../pages/setting_ai_view/ollama_setting.dart | 114 ------- .../pages/setting_ai_view/ollma_setting.dart | 168 ++++++++++ .../pages/setting_ai_view/plugin_state.dart | 248 ++++++++++++++ .../plugin_status_indicator.dart | 309 ------------------ frontend/resources/translations/en.json | 14 +- frontend/rust-lib/flowy-ai/src/entities.rs | 34 +- .../flowy-ai/src/local_ai/controller.rs | 23 +- .../flowy-ai/src/local_ai/resource.rs | 62 ++-- 17 files changed, 1037 insertions(+), 733 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart new file mode 100644 index 0000000000..185a8c049f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:url_launcher/url_launcher.dart' show launchUrl; +part 'download_offline_ai_app_bloc.freezed.dart'; + +class DownloadOfflineAIBloc + extends Bloc { + DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) { + on(_handleEvent); + } + + Future _handleEvent( + DownloadOfflineAIEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final result = await AIEventGetLocalAIDownloadLink().send(); + await result.fold( + (app) async { + await launchUrl(Uri.parse(app.link)); + }, + (err) {}, + ); + }, + ); + } +} + +@freezed +class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent { + const factory DownloadOfflineAIEvent.started() = _Started; +} + +@freezed +class DownloadOfflineAIState with _$DownloadOfflineAIState { + const factory DownloadOfflineAIState() = _DownloadOfflineAIState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 24befd9480..66ba7fe572 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -1,121 +1,94 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'local_llm_listener.dart'; - part 'local_ai_bloc.freezed.dart'; -class LocalAiPluginBloc extends Bloc { - LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) { - on(_handleEvent); - _startListening(); - _getLocalAiState(); - } - - final listener = LocalAIStateListener(); - - @override - Future close() async { - await listener.stop(); - return super.close(); +class LocalAIToggleBloc extends Bloc { + LocalAIToggleBloc() : super(const LocalAIToggleState()) { + on(_handleEvent); } Future _handleEvent( - LocalAiPluginEvent event, - Emitter emit, + LocalAIToggleEvent event, + Emitter emit, ) async { await event.when( - didReceiveAiState: (aiState) { + started: () async { + final result = await AIEventGetLocalAIState().send(); + _handleResult(emit, result); + }, + toggle: () async { emit( - ReadyLocalAiPluginState( - isEnabled: aiState.enabled, - runningState: aiState.state, - lackOfResource: - aiState.hasLackOfResource() ? aiState.lackOfResource : null, + state.copyWith( + pageIndicator: const LocalAIToggleStateIndicator.loading(), + ), + ); + unawaited( + AIEventToggleLocalAI().send().then( + (result) { + if (!isClosed) { + add(LocalAIToggleEvent.handleResult(result)); + } + }, ), ); }, - didReceiveLackOfResources: (resources) { - if (state case final ReadyLocalAiPluginState readyState) { - emit( - ReadyLocalAiPluginState( - isEnabled: readyState.isEnabled, - runningState: readyState.runningState, - lackOfResource: resources, - ), - ); - } + handleResult: (result) { + _handleResult(emit, result); }, - toggle: () async { - emit(LoadingLocalAiPluginState()); - await AIEventToggleLocalAI().send().fold( - (aiState) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - }, - Log.error, + ); + } + + void _handleResult( + Emitter emit, + FlowyResult result, + ) { + result.fold( + (localAI) { + emit( + state.copyWith( + pageIndicator: + LocalAIToggleStateIndicator.isEnabled(localAI.enabled), + ), ); }, - restart: () async { - emit(LoadingLocalAiPluginState()); - await AIEventRestartLocalAI().send(); + (err) { + emit( + state.copyWith( + pageIndicator: LocalAIToggleStateIndicator.error(err), + ), + ); }, ); } - - void _startListening() { - listener.start( - stateCallback: (pluginState) { - add(LocalAiPluginEvent.didReceiveAiState(pluginState)); - }, - resourceCallback: (data) { - add(LocalAiPluginEvent.didReceiveLackOfResources(data)); - }, - ); - } - - void _getLocalAiState() { - AIEventGetLocalAIState().send().fold( - (aiState) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - }, - Log.error, - ); - } } @freezed -class LocalAiPluginEvent with _$LocalAiPluginEvent { - const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) = - _DidReceiveAiState; - const factory LocalAiPluginEvent.didReceiveLackOfResources( - LackOfAIResourcePB resources, - ) = _DidReceiveLackOfResources; - const factory LocalAiPluginEvent.toggle() = _Toggle; - const factory LocalAiPluginEvent.restart() = _Restart; +class LocalAIToggleEvent with _$LocalAIToggleEvent { + const factory LocalAIToggleEvent.started() = _Started; + const factory LocalAIToggleEvent.toggle() = _Toggle; + const factory LocalAIToggleEvent.handleResult( + FlowyResult result, + ) = _HandleResult; } -sealed class LocalAiPluginState { - const LocalAiPluginState(); +@freezed +class LocalAIToggleState with _$LocalAIToggleState { + const factory LocalAIToggleState({ + @Default(LocalAIToggleStateIndicator.loading()) + LocalAIToggleStateIndicator pageIndicator, + }) = _LocalAIToggleState; } -class ReadyLocalAiPluginState extends LocalAiPluginState { - const ReadyLocalAiPluginState({ - required this.isEnabled, - required this.runningState, - required this.lackOfResource, - }); - - final bool isEnabled; - final RunningStatePB runningState; - final LackOfAIResourcePB? lackOfResource; -} - -class LoadingLocalAiPluginState extends LocalAiPluginState { - const LoadingLocalAiPluginState(); +@freezed +class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator { + // when start downloading the model + const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError; + const factory LocalAIToggleStateIndicator.isEnabled(bool isEnabled) = _Ready; + const factory LocalAIToggleStateIndicator.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 3bb26a182b..2c1bf34a87 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -26,7 +26,7 @@ class LocalAIOnBoardingBloc _dispatch(); } - void _onPaymentSuccessful() { + Future _onPaymentSuccessful() async { if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart new file mode 100644 index 0000000000..f6d5ef949d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'local_ai_setting_panel_bloc.freezed.dart'; + +class LocalAISettingPanelBloc + extends Bloc { + LocalAISettingPanelBloc() + : listener = LocalAIStateListener(), + super(const LocalAISettingPanelState()) { + on(_handleEvent); + + listener.start( + stateCallback: (newState) { + if (!isClosed) { + add(LocalAISettingPanelEvent.updateAIState(newState)); + } + }, + ); + + AIEventGetLocalAIState().send().fold( + (localAIState) { + if (!isClosed) { + add(LocalAISettingPanelEvent.updateAIState(localAIState)); + } + }, + Log.error, + ); + } + + final LocalAIStateListener listener; + + /// Handles incoming events and dispatches them to the appropriate handler. + Future _handleEvent( + LocalAISettingPanelEvent event, + Emitter emit, + ) async { + event.when( + updateAIState: (LocalAIPB pluginState) { + if (pluginState.isPluginExecutableReady) { + emit( + state.copyWith( + runningState: pluginState.state, + progressIndicator: const LocalAIProgress.checkPluginState(), + ), + ); + } else { + emit( + state.copyWith( + progressIndicator: const LocalAIProgress.downloadLocalAIApp(), + ), + ); + } + }, + ); + } + + @override + Future close() async { + await listener.stop(); + return super.close(); + } +} + +@freezed +class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent { + const factory LocalAISettingPanelEvent.updateAIState( + LocalAIPB aiState, + ) = _UpdateAIState; +} + +@freezed +class LocalAISettingPanelState with _$LocalAISettingPanelState { + const factory LocalAISettingPanelState({ + LocalAIProgress? progressIndicator, + @Default(RunningStatePB.Connecting) RunningStatePB runningState, + }) = _LocalAIChatSettingState; +} + +@freezed +class LocalAIProgress with _$LocalAIProgress { + const factory LocalAIProgress.checkPluginState() = _CheckPluginStateProgress; + const factory LocalAIProgress.downloadLocalAIApp() = + _DownloadLocalAIAppProgress; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart index f5c4209028..8555d8cdc8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart @@ -80,9 +80,11 @@ class OllamaSettingBloc extends Bloc { } add(OllamaSettingEvent.updateSetting(setting)); AIEventUpdateLocalAISetting(setting).send().fold( - (_) => Log.info('AI setting updated successfully'), - (err) => Log.error("update ai setting failed: $err"), - ); + (_) { + Log.info('AI setting updated successfully'); + }, + (err) => Log.error("update ai setting failed: $err"), + ); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart new file mode 100644 index 0000000000..4c3130ea00 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'plugin_state_bloc.freezed.dart'; + +class PluginStateBloc extends Bloc { + PluginStateBloc() + : listener = LocalAIStateListener(), + super( + const PluginStateState( + action: PluginStateAction.unknown(), + ), + ) { + listener.start( + stateCallback: (pluginState) { + if (!isClosed) { + add(PluginStateEvent.updateLocalAIState(pluginState)); + } + }, + resourceCallback: (data) { + if (!isClosed) { + add(PluginStateEvent.resourceStateChange(data)); + } + }, + ); + + on(_handleEvent); + } + + final LocalAIStateListener listener; + + @override + Future close() async { + await listener.stop(); + return super.close(); + } + + Future _handleEvent( + PluginStateEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final result = await AIEventGetLocalAIState().send(); + result.fold( + (pluginState) { + if (!isClosed) { + add(PluginStateEvent.updateLocalAIState(pluginState)); + } + }, + (err) => Log.error(err.toString()), + ); + }, + updateLocalAIState: (LocalAIPB aiState) { + // if the offline ai is not started, ask user to start it + if (aiState.hasLackOfResource()) { + emit( + PluginStateState( + action: PluginStateAction.lackOfResource(aiState.lackOfResource), + ), + ); + return; + } + + // Chech state of the plugin + switch (aiState.state) { + case RunningStatePB.ReadyToRun: + emit( + const PluginStateState( + action: PluginStateAction.readToRun(), + ), + ); + + case RunningStatePB.Connecting: + emit( + const PluginStateState( + action: PluginStateAction.initializingPlugin(), + ), + ); + case RunningStatePB.Connected: + emit( + const PluginStateState( + action: PluginStateAction.initializingPlugin(), + ), + ); + break; + case RunningStatePB.Running: + emit(const PluginStateState(action: PluginStateAction.running())); + break; + case RunningStatePB.Stopped: + emit( + state.copyWith(action: const PluginStateAction.restartPlugin()), + ); + default: + break; + } + }, + restartLocalAI: () async { + emit( + const PluginStateState(action: PluginStateAction.restartPlugin()), + ); + unawaited(AIEventRestartLocalAI().send()); + }, + resourceStateChange: (data) { + emit( + PluginStateState( + action: PluginStateAction.lackOfResource(data.resourceDesc), + ), + ); + }, + ); + } +} + +@freezed +class PluginStateEvent with _$PluginStateEvent { + const factory PluginStateEvent.started() = _Started; + const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) = + _UpdateLocalAIState; + const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; + const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) = + _ResourceStateChange; +} + +@freezed +class PluginStateState with _$PluginStateState { + const factory PluginStateState({ + required PluginStateAction action, + }) = _PluginStateState; +} + +@freezed +class PluginStateAction with _$PluginStateAction { + const factory PluginStateAction.unknown() = _Unknown; + const factory PluginStateAction.readToRun() = _ReadyToRun; + const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; + const factory PluginStateAction.running() = _PluginRunning; + const factory PluginStateAction.restartPlugin() = _RestartPlugin; + const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart new file mode 100644 index 0000000000..2de410a5e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class InitLocalAIIndicator extends StatelessWidget { + const InitLocalAIIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: BlocBuilder( + builder: (context, state) { + switch (state.runningState) { + case RunningStatePB.Connecting: + case RunningStatePB.Connected: + return Row( + children: [ + const HSpace(8), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing + .tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ), + ], + ); + case RunningStatePB.Running: + return SizedBox( + height: 30, + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ), + ); + case RunningStatePB.Stopped: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), + fontSize: 11, + color: const Color(0xFFC62828), + ), + ], + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 080dc426d1..57a72b6ca1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -1,174 +1,130 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'ollama_setting.dart'; -import 'plugin_status_indicator.dart'; - -class LocalAISetting extends StatefulWidget { +class LocalAISetting extends StatelessWidget { const LocalAISetting({super.key}); - @override - State createState() => _LocalAISettingState(); -} - -class _LocalAISettingState extends State { - final expandableController = ExpandableController(initialExpanded: false); - - @override - void dispose() { - expandableController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LocalAiPluginBloc(), - child: BlocConsumer( - listener: (context, state) { - final newIsExpanded = switch (state) { - final ReadyLocalAiPluginState readyLocalAiToggleState => - readyLocalAiToggleState.isEnabled, - _ => false, - }; - expandableController.value = newIsExpanded; - }, - builder: (context, state) { - return ExpandablePanel( - controller: expandableController, - theme: ExpandableThemeData( - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, + create: (context) => + LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ExpandableNotifier( + child: BlocListener( + listener: (context, state) { + final controller = + ExpandableController.of(context, required: true)!; + + state.pageIndicator.when( + error: (_) => controller.expanded = true, + isEnabled: (enabled) => controller.expanded = enabled, + loading: () => controller.expanded = true, + ); + }, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const LocalAISettingHeader(), + collapsed: const SizedBox.shrink(), + expanded: Column( + children: [ + const VSpace(12), + DecoratedBox( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: const Padding( + padding: + EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: LocalAISettingPanel(), + ), + ), + ], + ), ), - header: LocalAiSettingHeader( - isEnabled: state is ReadyLocalAiPluginState && state.isEnabled, - isToggleable: state is ReadyLocalAiPluginState, - ), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: EdgeInsets.only(top: 12), - child: LocalAISettingPanel(), - ), - ); - }, + ), + ), ), ); } } -class LocalAiSettingHeader extends StatelessWidget { - const LocalAiSettingHeader({ - super.key, - required this.isEnabled, - required this.isToggleable, - }); - - final bool isEnabled; - final bool isToggleable; +class LocalAISettingHeader extends StatelessWidget { + const LocalAISettingHeader({super.key}); @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), - ), - const VSpace(4), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), - maxLines: 3, - fontSize: 12, - ), - ], - ), - ), - IgnorePointer( - ignoring: !isToggleable, - child: Opacity( - opacity: isToggleable ? 1 : 0.5, - child: Toggle( - value: isEnabled, - onChanged: (_) => _onToggleChanged(context), - ), - ), - ), - ], - ); - } - - void _onToggleChanged(BuildContext context) { - if (isEnabled) { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), - description: - LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () { - context - .read() - .add(const LocalAiPluginEvent.toggle()); - }, - ); - } else { - context.read().add(const LocalAiPluginEvent.toggle()); - } - } -} - -class LocalAISettingPanel extends StatelessWidget { - const LocalAISettingPanel({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - if (state is! ReadyLocalAiPluginState) { - return const SizedBox.shrink(); - } - - final showIndicator = _showIndicator(state); - final showSettings = _showSettings(state); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showIndicator) const LocalAIStatusIndicator(), - if (showSettings && showIndicator) const VSpace(10), - if (showSettings) OllamaSettingPage(), - ], + return state.pageIndicator.when( + error: (error) => SizedBox.shrink(), + loading: () => const SizedBox.shrink(), + isEnabled: (isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FlowyText.medium( + LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + ), + const Spacer(), + Toggle( + value: isEnabled, + onChanged: (_) { + if (isEnabled) { + showConfirmDialog( + context: context, + title: LocaleKeys + .settings_aiPage_keys_disableLocalAITitle + .tr(), + description: LocaleKeys + .settings_aiPage_keys_disableLocalAIDescription + .tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () => context + .read() + .add(const LocalAIToggleEvent.toggle()), + ); + } else { + context + .read() + .add(const LocalAIToggleEvent.toggle()); + } + }, + ), + ], + ), + const VSpace(4), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), + maxLines: 3, + fontSize: 12, + ), + ], + ); + }, ); }, ); } - - bool _showIndicator(ReadyLocalAiPluginState state) { - return state.runningState != RunningStatePB.Running || - state.lackOfResource != null; - } - - bool _showSettings(ReadyLocalAiPluginState state) { - return ![ - RunningStatePB.Connecting, - RunningStatePB.Connected, - ].contains(state.runningState) && - (state.lackOfResource == null || - state.lackOfResource!.resourceType == - LackOfAIResourceTypePB.MissingModel); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart new file mode 100644 index 0000000000..5357db5c91 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'plugin_state.dart'; + +class LocalAISettingPanel extends StatelessWidget { + const LocalAISettingPanel({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LocalAISettingPanelBloc(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OllamaSettingPage(), + VSpace(6), + PluginStateIndicator(), + ], + ); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart deleted file mode 100644 index 2abcb552d1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class OllamaSettingPage extends StatelessWidget { - const OllamaSettingPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - OllamaSettingBloc()..add(const OllamaSettingEvent.started()), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.inputItems != current.inputItems || - previous.isEdited != current.isEdited, - builder: (context, state) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 10, - children: [ - for (final item in state.inputItems) - _SettingItemWidget(item: item), - _SaveButton(isEdited: state.isEdited), - ], - ), - ); - }, - ), - ); - } -} - -class _SettingItemWidget extends StatelessWidget { - const _SettingItemWidget({required this.item}); - - final SettingItem item; - - @override - Widget build(BuildContext context) { - return Column( - key: ValueKey(item.content + item.settingType.title), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - item.settingType.title, - fontSize: 12, - figmaLineHeight: 16, - ), - const VSpace(4), - SizedBox( - height: 32, - child: FlowyTextField( - hintText: item.hintText, - text: item.content, - onChanged: (content) { - context.read().add( - OllamaSettingEvent.onEdit(content, item.settingType), - ); - }, - ), - ), - ], - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({required this.isEdited}); - - final bool isEdited; - - @override - Widget build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.centerEnd, - child: FlowyTooltip( - message: isEdited ? null : 'No changes', - child: SizedBox( - child: FlowyButton( - text: FlowyText( - 'Apply', - figmaLineHeight: 20, - color: Theme.of(context).colorScheme.onPrimary, - ), - disable: !isEdited, - expandText: false, - margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200), - onTap: () { - if (isEdited) { - context - .read() - .add(const OllamaSettingEvent.submit()); - } - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart new file mode 100644 index 0000000000..8af4e35914 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart @@ -0,0 +1,168 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OllamaSettingPage extends StatelessWidget { + const OllamaSettingPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + OllamaSettingBloc()..add(const OllamaSettingEvent.started()), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.inputItems != current.inputItems || + previous.isEdited != current.isEdited, + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: state.inputItems.length, + separatorBuilder: (_, __) => const VSpace(10), + itemBuilder: (context, index) { + final item = state.inputItems[index]; + return _SettingItemWidget(item: item); + }, + ), + const VSpace(6), + Opacity( + opacity: 0.6, + child: _InstallOllamaInstruction(), + ), + _SaveButton(isEdited: state.isEdited), + ], + ); + }, + ), + ); + } +} + +class _SettingItemWidget extends StatelessWidget { + const _SettingItemWidget({required this.item}); + final SettingItem item; + + @override + Widget build(BuildContext context) { + return Column( + key: ValueKey(item.content + item.settingType.title), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText(item.settingType.title), + const VSpace(8), + FlowyTextField( + hintText: item.hintText, + text: item.content, + onChanged: (content) { + context.read().add( + OllamaSettingEvent.onEdit(content, item.settingType), + ); + }, + ), + ], + ); + } +} + +class _SaveButton extends StatelessWidget { + const _SaveButton({required this.isEdited}); + + final bool isEdited; + + @override + Widget build(BuildContext context) { + final tooltipMessage = isEdited ? 'Click to apply changes' : 'No changes'; + return SizedBox( + height: 50, + child: Row( + children: [ + const Spacer(), + SizedBox( + width: 120, + child: FlowyTooltip( + message: tooltipMessage, + child: Opacity( + opacity: isEdited ? 1 : 0.5, + child: FlowyTextButton( + 'Apply', + mainAxisAlignment: MainAxisAlignment.center, + onPressed: isEdited + ? () { + context + .read() + .add(const OllamaSettingEvent.submit()); + } + : null, + ), + ), + ), + ), + ], + ), + ); + } +} + +class _InstallOllamaInstruction extends StatelessWidget { + const _InstallOllamaInstruction(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: 3, + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString( + "https://appflowy.com/guide/appflowy-local-ai-ollama", + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + ], + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart new file mode 100644 index 0000000000..ab9303b429 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -0,0 +1,248 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PluginStateIndicator extends StatelessWidget { + const PluginStateIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + PluginStateBloc()..add(const PluginStateEvent.started()), + child: BlocBuilder( + builder: (context, state) { + return state.action.when( + unknown: () => const SizedBox.shrink(), + readToRun: () => const _PrepareRunning(), + initializingPlugin: () => const InitLocalAIIndicator(), + running: () => const _LocalAIRunning(), + restartPlugin: () => const _RestartPluginButton(), + lackOfResource: (desc) => _LackOfResource(desc: desc), + ); + }, + ), + ); + } +} + +class _PrepareRunning extends StatelessWidget { + const _PrepareRunning(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStart.tr(), + maxLines: 3, + ), + ), + ], + ); + } +} + +class _RestartPluginButton extends StatelessWidget { + const _RestartPluginButton(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const FlowySvg( + FlowySvgs.download_warn_s, + color: Color(0xFFC62828), + ), + const HSpace(6), + FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()), + const Spacer(), + SizedBox( + height: 30, + child: FlowyButton( + useIntrinsicWidth: true, + text: + FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()), + onTap: () { + context.read().add( + const PluginStateEvent.restartLocalAI(), + ); + }, + ), + ), + ], + ); + } +} + +class _LocalAIRunning extends StatelessWidget { + const _LocalAIRunning(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + Flexible( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class OpenOrDownloadOfflineAIApp extends StatelessWidget { + const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key}); + + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DownloadOfflineAIBloc(), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: 3, + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString( + "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline", + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = + () => context.read().add( + const DownloadOfflineAIEvent.started(), + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } +} + +class _LackOfResource extends StatelessWidget { + const _LackOfResource({required this.desc}); + + final String desc; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + FlowySvgs.toast_warning_filled_s, + size: const Size.square(20.0), + blendMode: null, + ), + const HSpace(6), + Expanded( + child: FlowyText( + desc, + maxLines: 3, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart deleted file mode 100644 index b79e228a0c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalAIStatusIndicator extends StatelessWidget { - const LocalAIStatusIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final readyState = switch (state) { - final ReadyLocalAiPluginState readyLocalAiToggleState => - readyLocalAiToggleState, - _ => null, - }; - if (readyState == null) { - return const SizedBox.shrink(); - } - - final lackOfResource = readyState.lackOfResource; - if (lackOfResource != null) { - return _LackOfResource(resource: lackOfResource); - } - - return switch (readyState.runningState) { - RunningStatePB.ReadyToRun => const _ReadyToRun(), - RunningStatePB.Connecting || - RunningStatePB.Connected => - _Initializing(), - RunningStatePB.Running => const _LocalAIRunning(), - RunningStatePB.Stopped => const _RestartPluginButton(), - _ => const SizedBox.shrink(), - }; - }, - ); - } -} - -class _ReadyToRun extends StatelessWidget { - const _ReadyToRun(); - - @override - Widget build(BuildContext context) { - return FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - maxLines: 3, - ); - } -} - -class _Initializing extends StatelessWidget { - const _Initializing(); - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - padding: const EdgeInsets.all(8.0), - width: double.infinity, - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ); - } -} - -class _RestartPluginButton extends StatelessWidget { - const _RestartPluginButton(); - - @override - Widget build(BuildContext context) { - final textStyle = - Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.toast_warning_filled_s, - color: Color(0xFFC62828), - ), - const HSpace(8), - Expanded( - child: RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: - LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning(); - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ), - ); - } -} - -class _LackOfResource extends StatelessWidget { - const _LackOfResource({required this.resource}); - - final LackOfAIResourcePB resource; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - FlowySvg( - FlowySvgs.toast_error_filled_s, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(8), - Expanded( - child: switch (resource.resourceType) { - LackOfAIResourceTypePB.PluginExecutableNotReady => - _buildNoLAI(context), - LackOfAIResourceTypePB.OllamaServerNotReady => - _buildNoOllama(context), - LackOfAIResourceTypePB.MissingModel => - _buildNoModel(context, resource.missingModelNames), - _ => const SizedBox.shrink(), - }, - ), - ], - ), - ); - } - - TextStyle? _textStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - } - - Widget _buildNoLAI(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: _textStyle(context)), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoOllama(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: textStyle), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoModel(BuildContext context, List modelNames) { - final textStyle = _textStyle(context); - - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - ], - ), - ); - } - - List _downloadInstructions(TextStyle? textStyle) { - return [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - TextSpan(text: ' ', style: textStyle), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr(), - style: textStyle, - ), - ]; - } -} - -void onDownloadButtonPressed() { - AIEventGetLocalAIDownloadLink().send().fold( - (app) => afLaunchUri(Uri.parse(app.link)), - (err) {}, - ); -} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 01c3a51b19..2b22da596c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -854,14 +854,14 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If it's slow, try toggling it off and on", + "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading. This may take a few minutes depending on your device", + "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", - "restartLocalAI": "Restart", + "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", "disableLocalAIDescription": "Do you want to disable local AI?", "localAIToggleTitle": "AppFlowy Local AI (LAI)", @@ -875,12 +875,10 @@ "activeOfflineAI": "Active", "downloadOfflineAI": "Download", "openModelDirectory": "Open folder", - "localAISetupInstruction1": "Please follow these", + "localAISetupInstruction1": "Follow these", "localAISetupInstruction2": "instructions", - "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI.", - "laiNotReady": "The Local AI app was not installed correctly.", - "ollamaNotReady": "The Ollama server is not ready.", - "modelsMissing": "Cannot find the model. You can use the ollama pull command to install them." + "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI. Skip if you've already set it up", + "startLocalAI": "It may take a few seconds to start the local AI" } }, "planPage": { diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index a7915aae23..b24d0cb13e 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -589,7 +589,7 @@ pub struct LocalAIPB { pub is_plugin_executable_ready: bool, #[pb(index = 3, one_of)] - pub lack_of_resource: Option, + pub lack_of_resource: Option, #[pb(index = 4)] pub state: RunningStatePB, @@ -724,35 +724,5 @@ impl From for LocalAISetting { #[derive(Default, ProtoBuf, Clone, Debug)] pub struct LackOfAIResourcePB { #[pb(index = 1)] - pub resource_type: LackOfAIResourceTypePB, - - #[pb(index = 2)] - pub missing_model_names: Vec, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum)] -pub enum LackOfAIResourceTypePB { - #[default] - PluginExecutableNotReady = 0, - OllamaServerNotReady = 1, - MissingModel = 2, -} - -impl From for LackOfAIResourcePB { - fn from(value: PendingResource) -> Self { - match value { - PendingResource::PluginExecutableNotReady => Self { - resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady, - missing_model_names: vec![], - }, - PendingResource::OllamaServerNotReady => Self { - resource_type: LackOfAIResourceTypePB::OllamaServerNotReady, - missing_model_names: vec![], - }, - PendingResource::MissingModel(model_name) => Self { - resource_type: LackOfAIResourceTypePB::MissingModel, - missing_model_names: vec![model_name], - }, - } - } + pub resource_desc: String, } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index bca710a907..c3f57e6fac 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -1,5 +1,5 @@ use crate::ai_manager::AIUserService; -use crate::entities::{LocalAIPB, RunningStatePB}; +use crate::entities::{LackOfAIResourcePB, LocalAIPB, RunningStatePB}; use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, @@ -274,15 +274,14 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let (is_plugin_executable_ready, state, lack_of_resource) = if enabled { - ( - is_plugin_ready(), - self.ai_plugin.get_plugin_running_state(), - self.resource.get_lack_of_resource().await, - ) - } else { - (false, RunningState::ReadyToConnect, None) - }; + let mut is_plugin_executable_ready = false; + let mut state = RunningState::ReadyToConnect; + let mut lack_of_resource = None; + if enabled { + is_plugin_executable_ready = is_plugin_ready(); + state = self.ai_plugin.get_plugin_running_state(); + lack_of_resource = self.resource.get_lack_of_resource().await; + } let elapsed = start.elapsed(); debug!( "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", @@ -483,7 +482,9 @@ async fn initialize_ai_plugin( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(lack_of_resource) + .payload(LackOfAIResourcePB { + resource_desc: lack_of_resource, + }) .send(); return Ok(()); diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index c40a9a9b88..3eddcf2039 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -47,13 +47,22 @@ pub enum WatchDiskEvent { Remove, } -#[derive(Debug, Clone)] pub enum PendingResource { PluginExecutableNotReady, OllamaServerNotReady, MissingModel(String), } +impl PendingResource { + pub fn desc(self) -> String { + match self { + PendingResource::PluginExecutableNotReady => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), + PendingResource::OllamaServerNotReady => "Ollama is not ready. Please follow the instructions to install Ollama".to_string(), + PendingResource::MissingModel(model) => format!("Cannot find the model: {}. Please use the ollama pull command to install the model", model), + } + } +} + pub struct LocalAIResourceController { user_service: Arc, resource_service: Arc, @@ -119,10 +128,10 @@ impl LocalAIResourceController { return false; } - self - .calculate_pending_resources() - .await - .is_ok_and(|r| r.is_none()) + match self.calculate_pending_resources().await { + Ok(res) => res.is_empty(), + Err(_) => false, + } } pub async fn get_plugin_download_link(&self) -> FlowyResult { @@ -138,35 +147,36 @@ impl LocalAIResourceController { #[instrument(level = "info", skip_all, err)] pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { self.resource_service.store_setting(setting)?; - if let Some(resource) = self.calculate_pending_resources().await? { + let mut resources = self.calculate_pending_resources().await?; + if let Some(resource) = resources.pop() { chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB::from(resource)) + .payload(LackOfAIResourcePB { + resource_desc: resource.desc(), + }) .send(); } Ok(()) } - pub async fn get_lack_of_resource(&self) -> Option { - self - .calculate_pending_resources() - .await - .ok()? - .map(Into::into) + pub async fn get_lack_of_resource(&self) -> Option { + let mut resources = self.calculate_pending_resources().await.ok()?; + resources.pop().map(|r| r.desc()) } - pub async fn calculate_pending_resources(&self) -> FlowyResult> { + pub async fn calculate_pending_resources(&self) -> FlowyResult> { + let mut resources = vec![]; let app_path = ollama_plugin_path(); if !is_plugin_ready() { trace!("[LLM Resource] offline app not found: {:?}", app_path); - return Ok(Some(PendingResource::PluginExecutableNotReady)); + resources.push(PendingResource::PluginExecutableNotReady); + return Ok(resources); } let setting = self.get_llm_setting(); let client = Client::builder().timeout(Duration::from_secs(5)).build()?; - match client.get(&setting.ollama_server_url).send().await { Ok(resp) if resp.status().is_success() => { info!( @@ -179,7 +189,8 @@ impl LocalAIResourceController { "[LLM Resource] Ollama server is not responding at {}", setting.ollama_server_url ); - return Ok(Some(PendingResource::OllamaServerNotReady)); + resources.push(PendingResource::OllamaServerNotReady); + return Ok(resources); }, } @@ -190,8 +201,12 @@ impl LocalAIResourceController { match client.get(&tags_url).send().await { Ok(resp) if resp.status().is_success() => { - let tags: TagsResponse = resp.json().await.inspect_err(|e| { - log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") + let tags: TagsResponse = resp.json().await.map_err(|e| { + log::error!( + "[LLM Resource] Failed to parse /api/tags JSON response: {:?}", + e + ); + e })?; // Check each required model is present in the response. for required in &required_models { @@ -200,7 +215,9 @@ impl LocalAIResourceController { "[LLM Resource] required model '{}' not found in API response", required ); - return Ok(Some(PendingResource::MissingModel(required.clone()))); + resources.push(PendingResource::MissingModel(required.clone())); + // Optionally, you could continue checking all models rather than returning early. + return Ok(resources); } } }, @@ -209,11 +226,12 @@ impl LocalAIResourceController { "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", setting.ollama_server_url ); - return Ok(Some(PendingResource::OllamaServerNotReady)); + resources.push(PendingResource::OllamaServerNotReady); + return Ok(resources); }, } - Ok(None) + Ok(resources) } #[instrument(level = "info", skip_all)] From af5c4bfe76c65882f130acac1577631c38db96c9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 09:09:40 +0800 Subject: [PATCH 247/384] chore: show plugin version --- .../pages/setting_ai_view/plugin_state.dart | 21 ++- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/en.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- .../flowy-ai/src/local_ai/controller.rs | 133 ++++++++++++------ 5 files changed, 109 insertions(+), 51 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index b8461eb141..12ce391a29 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -23,7 +23,10 @@ class PluginStateIndicator extends StatelessWidget { unknown: () => const SizedBox.shrink(), readToRun: () => const _PrepareRunning(), initializingPlugin: () => const InitLocalAIIndicator(), - running: (version) => _LocalAIRunning(version: version), + running: (version) => _LocalAIRunning( + key: ValueKey(version), + version: version, + ), restartPlugin: () => const _RestartPluginButton(), lackOfResource: (desc) => _LackOfResource(desc: desc), ); @@ -84,7 +87,7 @@ class _RestartPluginButton extends StatelessWidget { } class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning({required this.version}); + const _LocalAIRunning({required this.version, super.key}); final String version; @@ -111,13 +114,17 @@ class _LocalAIRunning extends StatelessWidget { color: Color(0xFF2E7D32), ), const HSpace(6), + if (version.isNotEmpty) + Flexible( + child: FlowyText( + "($version) ", + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ), Flexible( child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr( - args: [ - version, - ], - ), + LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), fontSize: 11, color: const Color(0xFF1E4620), maxLines: 3, diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index bb4534d855..e6a8be240c 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -857,7 +857,7 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل. الإصدار: {}", + "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8129157941..59877db7a6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -857,7 +857,7 @@ "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running. Version: {}", + "localAIRunning": "Local AI is running", "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 7ed8373c72..635efe92de 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -851,7 +851,7 @@ "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다. 버전: {}", + "localAIRunning": "로컬 AI가 실행 중입니다", "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 1bbe356e2b..03b014c89d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -77,62 +77,88 @@ impl LocalAIController { "[AI Plugin] init local ai controller, thread: {:?}", std::thread::current().id() ); + + // Create the core plugin and resource controller let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), cloud_service: cloud_service.clone(), store_preferences: store_preferences.clone(), }; - let local_ai_resource = Arc::new(LocalAIResourceController::new( user_service.clone(), res_impl, )); - let current_chat_id = ArcSwapOption::default(); + // Subscribe to state changes let mut running_state_rx = local_ai.subscribe_running_state(); - let cloned_llm_res = local_ai_resource.clone(); - let cloned_store_preferences = store_preferences.clone(); - let cloned_user_service = user_service.clone(); + + let cloned_llm_res = Arc::clone(&local_ai_resource); + let cloned_store_preferences = Arc::clone(&store_preferences); + let cloned_local_ai = Arc::clone(&local_ai); + let cloned_user_service = Arc::clone(&user_service); + + // Spawn a background task to listen for plugin state changes tokio::spawn(async move { while let Some(state) = running_state_rx.next().await { - if let Ok(workspace_id) = cloned_user_service.workspace_id() { - let key = local_ai_enabled_key(&workspace_id); - info!("[AI Plugin] state: {:?}", state); - let mut plugin_downloaded = false; - let mut lack_of_resource = None; - let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - plugin_downloaded = is_plugin_ready(); - lack_of_resource = cloned_llm_res.get_lack_of_resource().await; - } + // Skip if we can’t get workspace_id + let Ok(workspace_id) = cloned_user_service.workspace_id() else { + continue; + }; - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version: None, - }) - .send(); - } + let key = local_ai_enabled_key(&workspace_id); + info!("[AI Plugin] state: {:?}", state); + + // Read whether plugin is enabled from store; default to true + let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + + // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled + let (plugin_downloaded, lack_of_resource) = + if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { + // Possibly check plugin readiness and resource concurrency in parallel, + // but here we do it sequentially for clarity. + let downloaded = is_plugin_ready(); + let resource_lack = cloned_llm_res.get_lack_of_resource().await; + (downloaded, resource_lack) + } else { + (false, None) + }; + + // If plugin is running, retrieve version + let plugin_version = if matches!(state, RunningState::Running { .. }) { + match cloned_local_ai.plugin_info().await { + Ok(info) => Some(info.version), + Err(_) => None, + } + } else { + None + }; + + // Broadcast the new local AI state + let new_state = RunningStatePB::from(state); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + plugin_downloaded, + lack_of_resource, + state: new_state, + plugin_version, + }) + .send(); } }); Self { ai_plugin: local_ai, resource: local_ai_resource, - current_chat_id, + current_chat_id: ArcSwapOption::default(), store_preferences, user_service, cloud_service, } } - #[instrument(level = "debug", skip_all)] pub async fn observe_plugin_resource(&self) { debug!( @@ -275,22 +301,48 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let mut plugin_downloaded = false; - let mut state = RunningState::ReadyToConnect; - let mut lack_of_resource = None; - let mut plugin_version = None; - if enabled { - plugin_downloaded = is_plugin_ready(); - state = self.ai_plugin.get_plugin_running_state(); - plugin_version = self.ai_plugin.plugin_info().await.ok().map(|v| v.version); - lack_of_resource = self.resource.get_lack_of_resource().await; + + // If not enabled, return immediately. + if !enabled { + debug!( + "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", + start.elapsed(), + std::thread::current().id() + ); + return LocalAIPB { + enabled: false, + plugin_downloaded: false, + state: RunningStatePB::from(RunningState::ReadyToConnect), + lack_of_resource: None, + plugin_version: None, + }; } + + let plugin_downloaded = is_plugin_ready(); + let state = self.ai_plugin.get_plugin_running_state(); + + // If the plugin is running, run both requests in parallel. + // Otherwise, only fetch the resource info. + let (plugin_version, lack_of_resource) = if matches!(state, RunningState::Running { .. }) { + // Launch both futures at once + let plugin_info_fut = self.ai_plugin.plugin_info(); + let resource_fut = self.resource.get_lack_of_resource(); + + let (plugin_info_res, resource_res) = tokio::join!(plugin_info_fut, resource_fut); + let plugin_version = plugin_info_res.ok().map(|info| info.version); + (plugin_version, resource_res) + } else { + let resource_res = self.resource.get_lack_of_resource().await; + (None, resource_res) + }; + let elapsed = start.elapsed(); debug!( "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", elapsed, std::thread::current().id() ); + LocalAIPB { enabled, plugin_downloaded, @@ -299,7 +351,6 @@ impl LocalAIController { plugin_version, } } - #[instrument(level = "debug", skip_all)] pub async fn restart_plugin(&self) { if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { From 5ae3f423136c31452051e967c06141795640b177 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 11:28:29 +0800 Subject: [PATCH 248/384] chore: fix windows build --- frontend/rust-lib/Cargo.lock | 6 +++--- frontend/rust-lib/Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6bc02bda8a..69d6220409 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6baa7dea3e..9245232f29 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } From f76ce2be1481a4195b462632da6cd096bf5fdef0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 12:06:08 +0800 Subject: [PATCH 249/384] chore: show not ready state when using ai writer with local ai --- .../lib/ai/service/appflowy_ai_service.dart | 10 ++++++++++ .../ai/ai_writer_block_component.dart | 18 +++++++++++++++++ .../ai/operations/ai_writer_cubit.dart | 20 +++++++++++++++++++ .../ai_writer_test/ai_writer_bloc_test.dart | 4 ++++ frontend/resources/translations/en.json | 3 ++- frontend/rust-lib/flowy-ai/src/completion.rs | 15 ++++++++++---- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 18b1c71029..f2d666a586 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -28,6 +28,7 @@ abstract class AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }); } @@ -45,12 +46,14 @@ class AppFlowyAIService implements AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = AppFlowyCompletionStream( onStart: onStart, processMessage: processMessage, processAssistMessage: processAssistMessage, processError: onError, + onLocalAIInitializing: onLocalAIInitializing, onEnd: onEnd, ); @@ -85,6 +88,7 @@ abstract class CompletionStream { required this.processMessage, required this.processAssistMessage, required this.processError, + required this.onLocalAIInitializing, required this.onEnd, }); @@ -92,6 +96,7 @@ abstract class CompletionStream { final Future Function(String text) processMessage; final Future Function(String text) processAssistMessage; final void Function(AIError error) processError; + final void Function() onLocalAIInitializing; final Future Function() onEnd; } @@ -102,6 +107,7 @@ class AppFlowyCompletionStream extends CompletionStream { required super.processAssistMessage, required super.processError, required super.onEnd, + required super.onLocalAIInitializing, }) { _startListening(); } @@ -159,6 +165,10 @@ class AppFlowyCompletionStream extends CompletionStream { await onEnd(); } + if (event.startsWith("LOCAL_AI_NOT_READY:")) { + onLocalAIInitializing(); + } + if (event.startsWith("error:")) { processError( AIError(message: event.substring(6), code: AIErrorCode.other), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 769fabcd1f..1a94342f11 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -568,6 +568,24 @@ class MainContentArea extends StatelessWidget { ), ); } + if (state is LocalAIRunningAiWriterState) { + return Padding( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + const HSpace(8.0), + Opacity( + opacity: 0.5, + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), + ), + ), + const HSpace(8.0), + const CircularProgressIndicator.adaptive(), + ], + ), + ); + } return const SizedBox.shrink(); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 3275f0d75c..fae66d316c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -390,6 +390,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { @@ -481,6 +484,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -569,6 +575,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -639,6 +648,9 @@ class AiWriterCubit extends Cubit { } emit(ErrorAiWriterState(command, error: error)); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -714,3 +726,11 @@ class DocumentContentEmptyAiWriterState extends AiWriterState final void Function() onConfirm; } + +class LocalAIRunningAiWriterState extends AiWriterState + with RegisteredAiWriter { + const LocalAIRunningAiWriterState(this.command); + + @override + final AiWriterCommand command; +} diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index bffe27e985..0cd494dd4d 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -30,6 +30,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -62,6 +63,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -90,6 +92,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -120,6 +123,7 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 59877db7a6..72b46000a2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -858,7 +858,8 @@ "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", + "localAIInitializing": "Local AI is loading and may take a few seconds, depending on your device", + "localAINotReadyRetryLater": "Local AI is initializing, please retry later", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 38dea5f5e4..4a59cb0a8c 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -12,6 +12,7 @@ use flowy_error::{FlowyError, FlowyResult}; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; +use crate::stream_message::StreamMessage; use crate::util::ai_available_models_key; use flowy_sqlite::kv::KVStorePreferences; use std::sync::{Arc, Weak}; @@ -188,12 +189,18 @@ impl CompletionTask { } } -async fn handle_error(sink: &mut IsolateSink, error: FlowyError) { - if error.is_ai_response_limit_exceeded() { +async fn handle_error(sink: &mut IsolateSink, err: FlowyError) { + if err.is_ai_response_limit_exceeded() { let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await; - } else if error.is_ai_image_response_limit_exceeded() { + } else if err.is_ai_image_response_limit_exceeded() { let _ = sink.send("AI_IMAGE_RESPONSE_LIMIT".to_string()).await; + } else if err.is_ai_max_required() { + let _ = sink.send(format!("AI_MAX_REQUIRED:{}", err.msg)).await; + } else if err.is_local_ai_not_ready() { + let _ = sink.send(format!("LOCAL_AI_NOT_READY:{}", err.msg)).await; } else { - let _ = sink.send(format!("error:{}", error)).await; + let _ = sink + .send(StreamMessage::OnError(err.msg.clone()).to_string()) + .await; } } From 2277d7d23436c6cb2ecc62a2929e9578b2319ff0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 15:15:59 +0800 Subject: [PATCH 250/384] chore: show local ai disable --- .../lib/ai/service/ai_entities.dart | 14 +++ .../lib/ai/service/appflowy_ai_service.dart | 117 ++++++++++-------- .../application/chat_message_stream.dart | 34 ++--- .../ai/ai_writer_block_component.dart | 5 +- frontend/rust-lib/flowy-ai/src/chat.rs | 4 + frontend/rust-lib/flowy-ai/src/completion.rs | 2 + .../src/middleware/chat_service_mw.rs | 12 +- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 5 + 9 files changed, 116 insertions(+), 80 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart index 249a92019a..b08fadb7f8 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart @@ -4,6 +4,20 @@ import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; +class AIStreamEventPrefix { + static const data = 'data:'; + static const error = 'error:'; + static const metadata = 'metadata:'; + static const start = 'start:'; + static const finish = 'finish:'; + static const comment = 'comment:'; + static const aiResponseLimit = 'AI_RESPONSE_LIMIT'; + static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT'; + static const aiMaxRequired = 'AI_MAX_REQUIRED:'; + static const localAINotReady = 'LOCAL_AI_NOT_READY'; + static const localAIDisabled = 'LOCAL_AI_DISABLED'; +} + enum AiType { cloud, local; diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index f2d666a586..d96a3f1eed 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -121,59 +121,7 @@ class AppFlowyCompletionStream extends CompletionStream { _port.handler = _controller.add; _subscription = _controller.stream.listen( (event) async { - if (event == "AI_RESPONSE_LIMIT") { - processError( - AIError( - message: LocaleKeys.ai_textLimitReachedDescription.tr(), - code: AIErrorCode.aiResponseLimitExceeded, - ), - ); - } - - if (event == "AI_IMAGE_RESPONSE_LIMIT") { - processError( - AIError( - message: LocaleKeys.ai_imageLimitReachedDescription.tr(), - code: AIErrorCode.aiImageResponseLimitExceeded, - ), - ); - } - - if (event.startsWith("AI_MAX_REQUIRED:")) { - final msg = event.substring(16); - processError( - AIError( - message: msg, - code: AIErrorCode.other, - ), - ); - } - - if (event.startsWith("start:")) { - await onStart(); - } - - if (event.startsWith("data:")) { - await processMessage(event.substring(5)); - } - - if (event.startsWith("comment:")) { - await processAssistMessage(event.substring(8)); - } - - if (event.startsWith("finish:")) { - await onEnd(); - } - - if (event.startsWith("LOCAL_AI_NOT_READY:")) { - onLocalAIInitializing(); - } - - if (event.startsWith("error:")) { - processError( - AIError(message: event.substring(6), code: AIErrorCode.other), - ); - } + await _handleEvent(event); }, ); } @@ -183,4 +131,67 @@ class AppFlowyCompletionStream extends CompletionStream { await _subscription.cancel(); _port.close(); } + + Future _handleEvent(String event) async { + // Check simple matches first + if (event == AIStreamEventPrefix.aiResponseLimit) { + processError( + AIError( + message: LocaleKeys.ai_textLimitReachedDescription.tr(), + code: AIErrorCode.aiResponseLimitExceeded, + ), + ); + return; + } + + if (event == AIStreamEventPrefix.aiImageResponseLimit) { + processError( + AIError( + message: LocaleKeys.ai_imageLimitReachedDescription.tr(), + code: AIErrorCode.aiImageResponseLimitExceeded, + ), + ); + return; + } + + // Otherwise, parse out prefix:content + final colonIndex = event.indexOf(':'); + final hasColon = colonIndex != -1; + final prefix = hasColon ? event.substring(0, colonIndex) : event; + final content = hasColon ? event.substring(colonIndex + 1) : ''; + + switch (prefix) { + case AIStreamEventPrefix.aiMaxRequired: + processError(AIError(message: content, code: AIErrorCode.other)); + break; + + case AIStreamEventPrefix.start: + await onStart(); + break; + + case AIStreamEventPrefix.data: + await processMessage(content); + break; + + case AIStreamEventPrefix.comment: + await processAssistMessage(content); + break; + + case AIStreamEventPrefix.finish: + await onEnd(); + break; + + case AIStreamEventPrefix.localAINotReady: + onLocalAIInitializing(); + break; + + case AIStreamEventPrefix.error: + processError(AIError(message: content, code: AIErrorCode.other)); + break; + + default: + Log.debug('Unknown AI event: $event'); + break; + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart index 00d48e9347..c22559f21b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart @@ -2,19 +2,9 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; +import 'package:appflowy/ai/service/ai_entities.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart'; -/// Constants for event prefixes. -class AnswerEventPrefix { - static const data = 'data:'; - static const error = 'error:'; - static const metadata = 'metadata:'; - static const aiResponseLimit = 'AI_RESPONSE_LIMIT'; - static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT'; - static const aiMaxRequired = 'AI_MAX_REQUIRED:'; - static const localAINotReady = 'LOCAL_AI_NOT_READY'; -} - /// A stream that receives answer events from an isolate or external process. /// It caches events that might occur before a listener is attached. class AnswerStream { @@ -68,31 +58,31 @@ class AnswerStream { /// Handles incoming events from the underlying stream. void _handleEvent(String event) { - if (event.startsWith(AnswerEventPrefix.data)) { + if (event.startsWith(AIStreamEventPrefix.data)) { _hasStarted = true; - final newText = event.substring(AnswerEventPrefix.data.length); + final newText = event.substring(AIStreamEventPrefix.data.length); _text += newText; _onData?.call(_text); - } else if (event.startsWith(AnswerEventPrefix.error)) { - _error = event.substring(AnswerEventPrefix.error.length); + } else if (event.startsWith(AIStreamEventPrefix.error)) { + _error = event.substring(AIStreamEventPrefix.error.length); _onError?.call(_error!); - } else if (event.startsWith(AnswerEventPrefix.metadata)) { - final s = event.substring(AnswerEventPrefix.metadata.length); + } else if (event.startsWith(AIStreamEventPrefix.metadata)) { + final s = event.substring(AIStreamEventPrefix.metadata.length); _onMetadata?.call(parseMetadata(s)); - } else if (event == AnswerEventPrefix.aiResponseLimit) { + } else if (event == AIStreamEventPrefix.aiResponseLimit) { _aiLimitReached = true; _onAIResponseLimit?.call(); - } else if (event == AnswerEventPrefix.aiImageResponseLimit) { + } else if (event == AIStreamEventPrefix.aiImageResponseLimit) { _aiImageLimitReached = true; _onAIImageResponseLimit?.call(); - } else if (event.startsWith(AnswerEventPrefix.aiMaxRequired)) { - final msg = event.substring(AnswerEventPrefix.aiMaxRequired.length); + } else if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) { + final msg = event.substring(AIStreamEventPrefix.aiMaxRequired.length); if (_onAIMaxRequired != null) { _onAIMaxRequired!(msg); } else { _pendingAIMaxRequiredEvents.add(msg); } - } else if (event.startsWith(AnswerEventPrefix.localAINotReady)) { + } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) { if (_onLocalAIInitializing != null) { _onLocalAIInitializing!(); } else { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 1a94342f11..56b4934e3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -577,11 +577,10 @@ class MainContentArea extends StatelessWidget { Opacity( opacity: 0.5, child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), + LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater + .tr(), ), ), - const HSpace(8.0), - const CircularProgressIndicator.adaptive(), ], ), ); diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index ea553758df..e656aefa1e 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -289,6 +289,10 @@ impl Chat { let _ = answer_sink .send(format!("LOCAL_AI_NOT_READY:{}", err.msg)) .await; + } else if err.is_local_ai_disabled() { + let _ = answer_sink + .send(format!("LOCAL_AI_DISABLED:{}", err.msg)) + .await; } else { let _ = answer_sink .send(StreamMessage::OnError(err.msg.clone()).to_string()) diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 4a59cb0a8c..8e505a49b9 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -198,6 +198,8 @@ async fn handle_error(sink: &mut IsolateSink, err: FlowyError) { let _ = sink.send(format!("AI_MAX_REQUIRED:{}", err.msg)).await; } else if err.is_local_ai_not_ready() { let _ = sink.send(format!("LOCAL_AI_NOT_READY:{}", err.msg)).await; + } else if err.is_local_ai_disabled() { + let _ = sink.send(format!("LOCAL_AI_DISABLED:{}", err.msg)).await; } else { let _ = sink .send(StreamMessage::OnError(err.msg.clone()).to_string()) diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 41e7bcebd9..55f85ec5f9 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -181,7 +181,11 @@ impl ChatCloudService for AICloudServiceMiddleware { }, } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } else { self @@ -313,7 +317,11 @@ impl ChatCloudService for AICloudServiceMiddleware { }, } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } else { self diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index b16099013b..3288252ad2 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -377,6 +377,9 @@ pub enum ErrorCode { #[error("MCP error")] MCPError = 129, + + #[error("Local AI disabled")] + LocalAIDisabled = 130, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 0a6721a31a..4cef7a8990 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -99,6 +99,10 @@ impl FlowyError { self.code == ErrorCode::LocalAINotReady } + pub fn is_local_ai_disabled(&self) -> bool { + self.code == ErrorCode::LocalAIDisabled + } + pub fn is_ai_max_required(&self) -> bool { self.code == ErrorCode::AIMaxRequired } @@ -156,6 +160,7 @@ impl FlowyError { static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); + static_flowy_error!(local_ai_disabled, ErrorCode::LocalAIDisabled); } impl std::convert::From for FlowyError { From b63c4dfe212436be8cfcea8a478636e86f1d4322 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 20:12:20 +0800 Subject: [PATCH 251/384] chore: show local ai disable --- .../lib/ai/service/appflowy_ai_service.dart | 93 ++++++++++--------- .../ai/ai_writer_block_component.dart | 13 ++- .../ai/operations/ai_writer_cubit.dart | 25 +++-- .../ai_writer_test/ai_writer_bloc_test.dart | 12 ++- frontend/resources/translations/en.json | 1 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 17 +++- frontend/rust-lib/flowy-ai/src/chat.rs | 5 +- frontend/rust-lib/flowy-ai/src/completion.rs | 9 +- .../rust-lib/flowy-ai/src/event_handler.rs | 6 +- frontend/rust-lib/flowy-ai/src/event_map.rs | 6 +- 10 files changed, 106 insertions(+), 81 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index d96a3f1eed..39487652f8 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -15,6 +15,11 @@ import 'package:fixnum/fixnum.dart' as fixnum; import 'ai_entities.dart'; import 'error.dart'; +enum LocalAIStreamingState { + notReady, + disabled, +} + abstract class AIRepository { Future streamCompletion({ String? objectId, @@ -28,7 +33,8 @@ abstract class AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }); } @@ -46,14 +52,15 @@ class AppFlowyAIService implements AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }) async { final stream = AppFlowyCompletionStream( onStart: onStart, processMessage: processMessage, processAssistMessage: processAssistMessage, processError: onError, - onLocalAIInitializing: onLocalAIInitializing, + onLocalAIStreamingStateChange: onLocalAIStreamingStateChange, onEnd: onEnd, ); @@ -88,7 +95,7 @@ abstract class CompletionStream { required this.processMessage, required this.processAssistMessage, required this.processError, - required this.onLocalAIInitializing, + required this.onLocalAIStreamingStateChange, required this.onEnd, }); @@ -96,7 +103,8 @@ abstract class CompletionStream { final Future Function(String text) processMessage; final Future Function(String text) processAssistMessage; final void Function(AIError error) processError; - final void Function() onLocalAIInitializing; + final void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange; final Future Function() onEnd; } @@ -107,7 +115,7 @@ class AppFlowyCompletionStream extends CompletionStream { required super.processAssistMessage, required super.processError, required super.onEnd, - required super.onLocalAIInitializing, + required super.onLocalAIStreamingStateChange, }) { _startListening(); } @@ -155,43 +163,42 @@ class AppFlowyCompletionStream extends CompletionStream { } // Otherwise, parse out prefix:content - final colonIndex = event.indexOf(':'); - final hasColon = colonIndex != -1; - final prefix = hasColon ? event.substring(0, colonIndex) : event; - final content = hasColon ? event.substring(colonIndex + 1) : ''; - - switch (prefix) { - case AIStreamEventPrefix.aiMaxRequired: - processError(AIError(message: content, code: AIErrorCode.other)); - break; - - case AIStreamEventPrefix.start: - await onStart(); - break; - - case AIStreamEventPrefix.data: - await processMessage(content); - break; - - case AIStreamEventPrefix.comment: - await processAssistMessage(content); - break; - - case AIStreamEventPrefix.finish: - await onEnd(); - break; - - case AIStreamEventPrefix.localAINotReady: - onLocalAIInitializing(); - break; - - case AIStreamEventPrefix.error: - processError(AIError(message: content, code: AIErrorCode.other)); - break; - - default: - Log.debug('Unknown AI event: $event'); - break; + if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) { + processError( + AIError( + message: event.substring(AIStreamEventPrefix.aiMaxRequired.length), + code: AIErrorCode.other, + ), + ); + } else if (event.startsWith(AIStreamEventPrefix.start)) { + await onStart(); + } else if (event.startsWith(AIStreamEventPrefix.data)) { + await processMessage( + event.substring(AIStreamEventPrefix.data.length), + ); + } else if (event.startsWith(AIStreamEventPrefix.comment)) { + await processAssistMessage( + event.substring(AIStreamEventPrefix.comment.length), + ); + } else if (event.startsWith(AIStreamEventPrefix.finish)) { + await onEnd(); + } else if (event.startsWith(AIStreamEventPrefix.localAIDisabled)) { + onLocalAIStreamingStateChange( + LocalAIStreamingState.disabled, + ); + } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) { + onLocalAIStreamingStateChange( + LocalAIStreamingState.notReady, + ); + } else if (event.startsWith(AIStreamEventPrefix.error)) { + processError( + AIError( + message: event.substring(AIStreamEventPrefix.error.length), + code: AIErrorCode.other, + ), + ); + } else { + Log.debug('Unknown AI event: $event'); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 56b4934e3d..bfd7c24fea 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -568,7 +568,13 @@ class MainContentArea extends StatelessWidget { ), ); } - if (state is LocalAIRunningAiWriterState) { + if (state is LocalAIStreamingAiWriterState) { + final text = switch (state.state) { + LocalAIStreamingState.notReady => + LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater.tr(), + LocalAIStreamingState.disabled => + LocaleKeys.settings_aiPage_keys_localAIDisabled.tr(), + }; return Padding( padding: EdgeInsets.all(8.0), child: Row( @@ -576,10 +582,7 @@ class MainContentArea extends StatelessWidget { const HSpace(8.0), Opacity( opacity: 0.5, - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater - .tr(), - ), + child: FlowyText(text), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index fae66d316c..076046624a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -390,8 +390,8 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, - onLocalAIInitializing: () { - emit(LocalAIRunningAiWriterState(command)); + onLocalAIStreamingStateChange: (state) { + emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); @@ -484,8 +484,8 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, - onLocalAIInitializing: () { - emit(LocalAIRunningAiWriterState(command)); + onLocalAIStreamingStateChange: (state) { + emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); if (stream != null) { @@ -575,8 +575,8 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, - onLocalAIInitializing: () { - emit(LocalAIRunningAiWriterState(command)); + onLocalAIStreamingStateChange: (state) { + emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); if (stream != null) { @@ -648,8 +648,8 @@ class AiWriterCubit extends Cubit { } emit(ErrorAiWriterState(command, error: error)); }, - onLocalAIInitializing: () { - emit(LocalAIRunningAiWriterState(command)); + onLocalAIStreamingStateChange: (state) { + emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); if (stream != null) { @@ -727,10 +727,15 @@ class DocumentContentEmptyAiWriterState extends AiWriterState final void Function() onConfirm; } -class LocalAIRunningAiWriterState extends AiWriterState +class LocalAIStreamingAiWriterState extends AiWriterState with RegisteredAiWriter { - const LocalAIRunningAiWriterState(this.command); + const LocalAIStreamingAiWriterState( + this.command, { + required this.state, + }); @override final AiWriterCommand command; + + final LocalAIStreamingState state; } diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index 0cd494dd4d..bcd8b13d39 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -30,7 +30,8 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -63,7 +64,8 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -92,7 +94,8 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -123,7 +126,8 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function() onLocalAIInitializing, + required void Function(LocalAIStreamingState state) + onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 72b46000a2..e93f5273e5 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -860,6 +860,7 @@ "localAIRunning": "Local AI is running", "localAIInitializing": "Local AI is loading and may take a few seconds, depending on your device", "localAINotReadyRetryLater": "Local AI is initializing, please retry later", + "localAIDisabled": "You are using local AI, but it is disabled. Please go to settings to enable it or try different model", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index eabf41a1e3..d8ef5977d8 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -246,7 +246,8 @@ impl AIManager { params: StreamMessageParams, ) -> Result { let chat = self.get_or_create_chat_instance(¶ms.chat_id).await?; - let question = chat.stream_chat_message(¶ms).await?; + let ai_model = self.get_active_model(¶ms.chat_id).await; + let question = chat.stream_chat_message(¶ms, ai_model).await?; let _ = self .external_service .notify_did_send_message(¶ms.chat_id, ¶ms.message) @@ -394,6 +395,20 @@ impl AIManager { Ok(()) } + pub async fn get_active_model(&self, source: &str) -> Option { + let mut model = self + .store_preferences + .get_object::(&ai_available_models_key(source)); + + if model.is_none() { + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + model = Some(AIModel::local(local_model, "".to_string())); + } + } + + model + } + pub async fn get_available_models(&self, source: String) -> FlowyResult { // Build the models list from server models and mark them as non-local. let mut models: Vec = self diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index e656aefa1e..50cec2c625 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -10,7 +10,6 @@ use crate::persistence::{ ChatMessageTable, }; use crate::stream_message::StreamMessage; -use crate::util::ai_available_models_key; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, @@ -89,6 +88,7 @@ impl Chat { pub async fn stream_chat_message( &self, params: &StreamMessageParams, + preferred_ai_model: Option, ) -> Result { trace!( "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", @@ -143,9 +143,6 @@ impl Chat { // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); - let preferred_ai_model = self - .store_preferences - .get_object::(&ai_available_models_key(&self.chat_id)); self.stream_response( params.answer_stream_port, answer_stream_buffer, diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 8e505a49b9..393685d642 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -13,7 +13,6 @@ use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; use crate::stream_message::StreamMessage; -use crate::util::ai_available_models_key; use flowy_sqlite::kv::KVStorePreferences; use std::sync::{Arc, Weak}; use tokio::select; @@ -23,26 +22,24 @@ pub struct AICompletion { tasks: Arc>>, cloud_service: Weak, user_service: Weak, - store_preferences: Arc, } impl AICompletion { pub fn new( cloud_service: Weak, user_service: Weak, - store_preferences: Arc, ) -> Self { Self { tasks: Arc::new(DashMap::new()), cloud_service, user_service, - store_preferences, } } pub async fn create_complete_task( &self, complete: CompleteTextPB, + preferred_model: Option, ) -> FlowyResult { if matches!(complete.completion_type, CompletionTypePB::CustomPrompt) && complete.custom_prompt.is_none() @@ -59,10 +56,6 @@ impl AICompletion { .ok_or_else(FlowyError::internal)? .workspace_id()?; let (tx, rx) = tokio::sync::mpsc::channel(1); - let preferred_model = self - .store_preferences - .get_object::(&ai_available_models_key(&complete.object_id)); - let task = CompletionTask::new( workspace_id, complete, diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 3150d3e378..4a36a5d37a 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -209,9 +209,13 @@ pub(crate) async fn stop_stream_handler( pub(crate) async fn start_complete_text_handler( data: AFPluginData, + ai_manager: AFPluginState>, tools: AFPluginState>, ) -> DataResult { - let task = tools.create_complete_task(data.into_inner()).await?; + let data = data.into_inner(); + let ai_manager = upgrade_ai_manager(ai_manager)?; + let ai_model = ai_manager.get_active_model(&data.object_id).await; + let task = tools.create_complete_task(data, ai_model).await?; data_result_ok(task) } diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 51c49eaabb..5020836a30 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -13,11 +13,7 @@ pub fn init(ai_manager: Weak) -> AFPlugin { let strong_ai_manager = ai_manager.upgrade().unwrap(); let user_service = Arc::downgrade(&strong_ai_manager.user_service); let cloud_service = Arc::downgrade(&strong_ai_manager.cloud_service_wm); - let ai_tools = Arc::new(AICompletion::new( - cloud_service, - user_service, - strong_ai_manager.store_preferences.clone(), - )); + let ai_tools = Arc::new(AICompletion::new(cloud_service, user_service)); AFPlugin::new() .name("flowy-ai") .state(ai_manager) From e3a0806eee2e24cc47f015655fa09f5fd8a97b1e Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 20:53:27 +0800 Subject: [PATCH 252/384] chore: clippy --- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 3 --- frontend/rust-lib/flowy-ai/src/chat.rs | 4 ---- frontend/rust-lib/flowy-ai/src/completion.rs | 1 - .../flowy-ai/src/middleware/chat_service_mw.rs | 16 ++++++---------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index d8ef5977d8..1aac5b0109 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -139,7 +139,6 @@ impl AIManager { chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), - self.store_preferences.clone(), )) }); if self.local_ai.is_running() { @@ -235,7 +234,6 @@ impl AIManager { chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), - self.store_preferences.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) @@ -495,7 +493,6 @@ impl AIManager { chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), - self.store_preferences.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 50cec2c625..e00f21a863 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -15,7 +15,6 @@ use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; @@ -40,7 +39,6 @@ pub struct Chat { latest_message_id: Arc, stop_stream: Arc, stream_buffer: Arc>, - store_preferences: Arc, } impl Chat { @@ -49,7 +47,6 @@ impl Chat { chat_id: String, user_service: Arc, chat_service: Arc, - store_preferences: Arc, ) -> Chat { Chat { uid, @@ -60,7 +57,6 @@ impl Chat { latest_message_id: Default::default(), stop_stream: Arc::new(AtomicBool::new(false)), stream_buffer: Arc::new(Mutex::new(StringBuffer::default())), - store_preferences, } } diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 393685d642..27babfb28e 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -13,7 +13,6 @@ use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; use crate::stream_message::StreamMessage; -use flowy_sqlite::kv::KVStorePreferences; use std::sync::{Arc, Weak}; use tokio::select; use tracing::info; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 55f85ec5f9..dd294e6f0e 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -180,12 +180,10 @@ impl ChatCloudService for AICloudServiceMiddleware { Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) }, } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } else { self @@ -316,12 +314,10 @@ impl ChatCloudService for AICloudServiceMiddleware { Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) }, } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } else { self From a2303d35e858616e394789bf0d3cb3c3847e902b Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:24:46 +0800 Subject: [PATCH 253/384] chore: hide formats when using specific ai writer commands (#7648) * chore: hide formats for specific ai writer features * chore: use black color for selected model name --- .../desktop_prompt_text_field.dart | 27 ++++++++++++------- .../prompt_input/select_model_menu.dart | 1 - .../ai/ai_writer_block_component.dart | 6 +++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index 97850f6a1c..a2676f2c15 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart @@ -23,6 +23,7 @@ class DesktopPromptInput extends StatefulWidget { required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, this.hideDecoration = false, + this.hideFormats = false, this.extraBottomActionButton, }); @@ -34,6 +35,7 @@ class DesktopPromptInput extends StatefulWidget { final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; final bool hideDecoration; + final bool hideFormats; final Widget? extraBottomActionButton; @override @@ -139,11 +141,11 @@ class _DesktopPromptInputState extends State { children: [ ConstrainedBox( constraints: getTextFieldConstraints( - state.showPredefinedFormats, + state.showPredefinedFormats && !widget.hideFormats, ), child: inputTextField(), ), - if (state.showPredefinedFormats) + if (state.showPredefinedFormats && !widget.hideFormats) Positioned.fill( bottom: null, child: TextFieldTapRegion( @@ -168,8 +170,9 @@ class _DesktopPromptInputState extends State { top: null, child: TextFieldTapRegion( child: _PromptBottomActions( - showPredefinedFormats: + showPredefinedFormatBar: state.showPredefinedFormats, + showPredefinedFormatButton: !widget.hideFormats, onTogglePredefinedFormatSection: () => context.read().add( AIPromptInputEvent @@ -571,7 +574,8 @@ class PromptInputTextField extends StatelessWidget { class _PromptBottomActions extends StatelessWidget { const _PromptBottomActions({ required this.sendButtonState, - required this.showPredefinedFormats, + required this.showPredefinedFormatBar, + required this.showPredefinedFormatButton, required this.onTogglePredefinedFormatSection, required this.onStartMention, required this.onSendPressed, @@ -581,7 +585,8 @@ class _PromptBottomActions extends StatelessWidget { this.extraBottomActionButton, }); - final bool showPredefinedFormats; + final bool showPredefinedFormatBar; + final bool showPredefinedFormatButton; final void Function() onTogglePredefinedFormatSection; final void Function() onStartMention; final SendButtonState sendButtonState; @@ -600,10 +605,12 @@ class _PromptBottomActions extends StatelessWidget { builder: (context, state) { return Row( children: [ - _predefinedFormatButton(), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), + if (showPredefinedFormatButton) ...[ + _predefinedFormatButton(), + const HSpace( + DesktopAIChatSizes.inputActionBarButtonSpacing, + ), + ], SelectModelMenu( aiModelStateNotifier: context.read().aiModelStateNotifier, @@ -641,7 +648,7 @@ class _PromptBottomActions extends StatelessWidget { Widget _predefinedFormatButton() { return PromptInputDesktopToggleFormatButton( - showFormatBar: showPredefinedFormats, + showFormatBar: showPredefinedFormatBar, onTap: onTogglePredefinedFormatSection, ); } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index 64a62b902c..eef2663370 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -173,7 +173,6 @@ class _ModelItem extends StatelessWidget { model.i18n, figmaLineHeight: 20, overflow: TextOverflow.ellipsis, - color: isSelected ? Theme.of(context).colorScheme.primary : null, ), if (model.desc.isNotEmpty) FlowyText( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index bfd7c24fea..f78f7d35fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -490,6 +490,12 @@ class MainContentArea extends StatelessWidget { return DesktopPromptInput( isStreaming: false, hideDecoration: true, + hideFormats: [ + AiWriterCommand.fixSpellingAndGrammar, + AiWriterCommand.improveWriting, + AiWriterCommand.makeLonger, + AiWriterCommand.makeShorter, + ].contains(state.command), textController: textController, onSubmitted: (message, format, _) { cubit.runCommand(state.command, message, format); From 34d2b7f24e3ed3b4feb7e9599c4be462ecda068e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:05:11 +0800 Subject: [PATCH 254/384] chore: improve local ai settings page (#7651) * chore: improve local ai settings page * chore: move local to top in select list * chore: improve copy of missing model * chore: remove green background and add progress indicator * chore: change text color --- .../settings/ai/local_ai_bloc.dart | 171 ++++++--- .../ai/local_ai_on_boarding_bloc.dart | 2 +- .../ai/local_ai_setting_panel_bloc.dart | 92 ----- .../settings/ai/ollama_setting_bloc.dart | 8 +- .../settings/ai/plugin_state_bloc.dart | 150 -------- .../pages/setting_ai_view/init_local_ai.dart | 82 ---- .../setting_ai_view/local_ai_setting.dart | 222 ++++++----- .../local_ai_setting_panel.dart | 32 -- .../setting_ai_view/model_selection.dart | 19 +- .../pages/setting_ai_view/ollama_setting.dart | 114 ++++++ .../pages/setting_ai_view/ollma_setting.dart | 168 -------- .../pages/setting_ai_view/plugin_state.dart | 168 -------- .../plugin_status_indicator.dart | 359 ++++++++++++++++++ frontend/resources/translations/ar-SA.json | 8 +- frontend/resources/translations/en.json | 21 +- frontend/resources/translations/ko-KR.json | 8 +- frontend/rust-lib/flowy-ai/src/entities.rs | 34 +- .../flowy-ai/src/local_ai/controller.rs | 6 +- .../flowy-ai/src/local_ai/resource.rs | 62 ++- 19 files changed, 798 insertions(+), 928 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 66ba7fe572..c4a4ee3afa 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -1,94 +1,145 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'local_llm_listener.dart'; + part 'local_ai_bloc.freezed.dart'; -class LocalAIToggleBloc extends Bloc { - LocalAIToggleBloc() : super(const LocalAIToggleState()) { - on(_handleEvent); +class LocalAiPluginBloc extends Bloc { + LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) { + on(_handleEvent); + _startListening(); + _getLocalAiState(); + } + + final listener = LocalAIStateListener(); + + @override + Future close() async { + await listener.stop(); + return super.close(); } Future _handleEvent( - LocalAIToggleEvent event, - Emitter emit, + LocalAiPluginEvent event, + Emitter emit, ) async { await event.when( - started: () async { - final result = await AIEventGetLocalAIState().send(); - _handleResult(emit, result); + didReceiveAiState: (aiState) { + emit( + LocalAiPluginState.ready( + isEnabled: aiState.enabled, + version: aiState.pluginVersion, + runningState: aiState.state, + lackOfResource: + aiState.hasLackOfResource() ? aiState.lackOfResource : null, + ), + ); + }, + didReceiveLackOfResources: (resources) { + state.maybeMap( + ready: (readyState) { + emit(readyState.copyWith(lackOfResource: resources)); + }, + orElse: () {}, + ); }, toggle: () async { - emit( - state.copyWith( - pageIndicator: const LocalAIToggleStateIndicator.loading(), - ), - ); - unawaited( - AIEventToggleLocalAI().send().then( - (result) { - if (!isClosed) { - add(LocalAIToggleEvent.handleResult(result)); - } - }, - ), + emit(LocalAiPluginState.loading()); + await AIEventToggleLocalAI().send().fold( + (aiState) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + }, + Log.error, ); }, - handleResult: (result) { - _handleResult(emit, result); + restart: () async { + emit(LocalAiPluginState.loading()); + await AIEventRestartLocalAI().send(); }, ); } - void _handleResult( - Emitter emit, - FlowyResult result, - ) { - result.fold( - (localAI) { - emit( - state.copyWith( - pageIndicator: - LocalAIToggleStateIndicator.isEnabled(localAI.enabled), - ), - ); + void _startListening() { + listener.start( + stateCallback: (pluginState) { + add(LocalAiPluginEvent.didReceiveAiState(pluginState)); }, - (err) { - emit( - state.copyWith( - pageIndicator: LocalAIToggleStateIndicator.error(err), - ), - ); + resourceCallback: (data) { + add(LocalAiPluginEvent.didReceiveLackOfResources(data)); }, ); } + + void _getLocalAiState() { + AIEventGetLocalAIState().send().fold( + (aiState) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + }, + Log.error, + ); + } } @freezed -class LocalAIToggleEvent with _$LocalAIToggleEvent { - const factory LocalAIToggleEvent.started() = _Started; - const factory LocalAIToggleEvent.toggle() = _Toggle; - const factory LocalAIToggleEvent.handleResult( - FlowyResult result, - ) = _HandleResult; +class LocalAiPluginEvent with _$LocalAiPluginEvent { + const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) = + _DidReceiveAiState; + const factory LocalAiPluginEvent.didReceiveLackOfResources( + LackOfAIResourcePB resources, + ) = _DidReceiveLackOfResources; + const factory LocalAiPluginEvent.toggle() = _Toggle; + const factory LocalAiPluginEvent.restart() = _Restart; } @freezed -class LocalAIToggleState with _$LocalAIToggleState { - const factory LocalAIToggleState({ - @Default(LocalAIToggleStateIndicator.loading()) - LocalAIToggleStateIndicator pageIndicator, - }) = _LocalAIToggleState; -} +class LocalAiPluginState with _$LocalAiPluginState { + const LocalAiPluginState._(); -@freezed -class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator { - // when start downloading the model - const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError; - const factory LocalAIToggleStateIndicator.isEnabled(bool isEnabled) = _Ready; - const factory LocalAIToggleStateIndicator.loading() = _Loading; + const factory LocalAiPluginState.ready({ + required bool isEnabled, + required String version, + required RunningStatePB runningState, + required LackOfAIResourcePB? lackOfResource, + }) = ReadyLocalAiPluginState; + + const factory LocalAiPluginState.loading() = LoadingLocalAiPluginState; + + bool get isEnabled { + return maybeWhen( + ready: (isEnabled, _, __, ___) => isEnabled, + orElse: () => false, + ); + } + + bool get showIndicator { + return maybeWhen( + ready: (isEnabled, _, runningState, lackOfResource) => + runningState != RunningStatePB.Running || lackOfResource != null, + orElse: () => false, + ); + } + + bool get showSettings { + return maybeWhen( + ready: (isEnabled, _, runningState, lackOfResource) { + final isConnecting = [ + RunningStatePB.Connecting, + RunningStatePB.Connected, + ].contains(runningState); + + final resourcesReadyOrMissingModel = lackOfResource == null || + lackOfResource.resourceType == LackOfAIResourceTypePB.MissingModel; + + return !isConnecting && resourcesReadyOrMissingModel; + }, + orElse: () => false, + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 2c1bf34a87..3bb26a182b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -26,7 +26,7 @@ class LocalAIOnBoardingBloc _dispatch(); } - Future _onPaymentSuccessful() async { + void _onPaymentSuccessful() { if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart deleted file mode 100644 index 60c68b70c6..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'local_ai_setting_panel_bloc.freezed.dart'; - -class LocalAISettingPanelBloc - extends Bloc { - LocalAISettingPanelBloc() - : listener = LocalAIStateListener(), - super(const LocalAISettingPanelState()) { - on(_handleEvent); - - listener.start( - stateCallback: (newState) { - if (!isClosed) { - add(LocalAISettingPanelEvent.updateAIState(newState)); - } - }, - ); - - AIEventGetLocalAIState().send().fold( - (localAIState) { - if (!isClosed) { - add(LocalAISettingPanelEvent.updateAIState(localAIState)); - } - }, - Log.error, - ); - } - - final LocalAIStateListener listener; - - /// Handles incoming events and dispatches them to the appropriate handler. - Future _handleEvent( - LocalAISettingPanelEvent event, - Emitter emit, - ) async { - event.when( - updateAIState: (LocalAIPB pluginState) { - if (pluginState.pluginDownloaded) { - emit( - state.copyWith( - runningState: pluginState.state, - progressIndicator: const LocalAIProgress.checkPluginState(), - ), - ); - } else { - emit( - state.copyWith( - progressIndicator: const LocalAIProgress.downloadLocalAIApp(), - ), - ); - } - }, - ); - } - - @override - Future close() async { - await listener.stop(); - return super.close(); - } -} - -@freezed -class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent { - const factory LocalAISettingPanelEvent.updateAIState( - LocalAIPB aiState, - ) = _UpdateAIState; -} - -@freezed -class LocalAISettingPanelState with _$LocalAISettingPanelState { - const factory LocalAISettingPanelState({ - LocalAIProgress? progressIndicator, - @Default(RunningStatePB.Connecting) RunningStatePB runningState, - }) = _LocalAIChatSettingState; -} - -@freezed -class LocalAIProgress with _$LocalAIProgress { - const factory LocalAIProgress.checkPluginState() = _CheckPluginStateProgress; - const factory LocalAIProgress.downloadLocalAIApp() = - _DownloadLocalAIAppProgress; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart index 8555d8cdc8..f5c4209028 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart @@ -80,11 +80,9 @@ class OllamaSettingBloc extends Bloc { } add(OllamaSettingEvent.updateSetting(setting)); AIEventUpdateLocalAISetting(setting).send().fold( - (_) { - Log.info('AI setting updated successfully'); - }, - (err) => Log.error("update ai setting failed: $err"), - ); + (_) => Log.info('AI setting updated successfully'), + (err) => Log.error("update ai setting failed: $err"), + ); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart deleted file mode 100644 index d91d7151ab..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'plugin_state_bloc.freezed.dart'; - -class PluginStateBloc extends Bloc { - PluginStateBloc() - : listener = LocalAIStateListener(), - super( - const PluginStateState( - action: PluginStateAction.unknown(), - ), - ) { - listener.start( - stateCallback: (pluginState) { - if (!isClosed) { - add(PluginStateEvent.updateLocalAIState(pluginState)); - } - }, - resourceCallback: (data) { - if (!isClosed) { - add(PluginStateEvent.resourceStateChange(data)); - } - }, - ); - - on(_handleEvent); - } - - final LocalAIStateListener listener; - - @override - Future close() async { - await listener.stop(); - return super.close(); - } - - Future _handleEvent( - PluginStateEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIState().send(); - result.fold( - (pluginState) { - if (!isClosed) { - add(PluginStateEvent.updateLocalAIState(pluginState)); - } - }, - (err) => Log.error(err.toString()), - ); - }, - updateLocalAIState: (LocalAIPB aiState) { - // if the offline ai is not started, ask user to start it - if (aiState.hasLackOfResource()) { - emit( - PluginStateState( - action: PluginStateAction.lackOfResource(aiState.lackOfResource), - ), - ); - return; - } - - // Chech state of the plugin - switch (aiState.state) { - case RunningStatePB.ReadyToRun: - emit( - const PluginStateState( - action: PluginStateAction.readToRun(), - ), - ); - - case RunningStatePB.Connecting: - emit( - const PluginStateState( - action: PluginStateAction.initializingPlugin(), - ), - ); - case RunningStatePB.Connected: - emit( - const PluginStateState( - action: PluginStateAction.initializingPlugin(), - ), - ); - break; - case RunningStatePB.Running: - emit( - PluginStateState( - action: PluginStateAction.running(aiState.pluginVersion), - ), - ); - break; - case RunningStatePB.Stopped: - emit( - state.copyWith(action: const PluginStateAction.restartPlugin()), - ); - default: - break; - } - }, - restartLocalAI: () async { - emit( - const PluginStateState(action: PluginStateAction.restartPlugin()), - ); - unawaited(AIEventRestartLocalAI().send()); - }, - resourceStateChange: (data) { - emit( - PluginStateState( - action: PluginStateAction.lackOfResource(data.resourceDesc), - ), - ); - }, - ); - } -} - -@freezed -class PluginStateEvent with _$PluginStateEvent { - const factory PluginStateEvent.started() = _Started; - const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) = - _UpdateLocalAIState; - const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; - const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) = - _ResourceStateChange; -} - -@freezed -class PluginStateState with _$PluginStateState { - const factory PluginStateState({ - required PluginStateAction action, - }) = _PluginStateState; -} - -@freezed -class PluginStateAction with _$PluginStateAction { - const factory PluginStateAction.unknown() = _Unknown; - const factory PluginStateAction.readToRun() = _ReadyToRun; - const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; - const factory PluginStateAction.running(String version) = _PluginRunning; - const factory PluginStateAction.restartPlugin() = _RestartPlugin; - const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart deleted file mode 100644 index 2de410a5e5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class InitLocalAIIndicator extends StatelessWidget { - const InitLocalAIIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all( - Radius.circular(4), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: BlocBuilder( - builder: (context, state) { - switch (state.runningState) { - case RunningStatePB.Connecting: - case RunningStatePB.Connected: - return Row( - children: [ - const HSpace(8), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing - .tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ); - case RunningStatePB.Running: - return SizedBox( - height: 30, - child: Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ], - ), - ); - case RunningStatePB.Stopped: - return Row( - children: [ - const HSpace(8), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), - fontSize: 11, - color: const Color(0xFFC62828), - ), - ], - ); - default: - return const SizedBox.shrink(); - } - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 57a72b6ca1..751e7a6180 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -1,128 +1,150 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class LocalAISetting extends StatelessWidget { +import 'ollama_setting.dart'; +import 'plugin_status_indicator.dart'; + +class LocalAISetting extends StatefulWidget { const LocalAISetting({super.key}); + @override + State createState() => _LocalAISettingState(); +} + +class _LocalAISettingState extends State { + final expandableController = ExpandableController(initialExpanded: false); + + @override + void dispose() { + expandableController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: ExpandableNotifier( - child: BlocListener( - listener: (context, state) { - final controller = - ExpandableController.of(context, required: true)!; - - state.pageIndicator.when( - error: (_) => controller.expanded = true, - isEnabled: (enabled) => controller.expanded = enabled, - loading: () => controller.expanded = true, - ); - }, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: const LocalAISettingHeader(), - collapsed: const SizedBox.shrink(), - expanded: Column( - children: [ - const VSpace(12), - DecoratedBox( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: LocalAISettingPanel(), - ), - ), - ], - ), + create: (context) => LocalAiPluginBloc(), + child: BlocConsumer( + listener: (context, state) { + expandableController.value = state.isEnabled; + }, + builder: (context, state) { + return ExpandablePanel( + controller: expandableController, + theme: ExpandableThemeData( + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, ), - ), - ), + header: LocalAiSettingHeader( + isEnabled: state.isEnabled, + isToggleable: state is ReadyLocalAiPluginState, + ), + collapsed: const SizedBox.shrink(), + expanded: Padding( + padding: EdgeInsets.only(top: 12), + child: LocalAISettingPanel(), + ), + ); + }, ), ); } } -class LocalAISettingHeader extends StatelessWidget { - const LocalAISettingHeader({super.key}); +class LocalAiSettingHeader extends StatelessWidget { + const LocalAiSettingHeader({ + super.key, + required this.isEnabled, + required this.isToggleable, + }); + + final bool isEnabled; + final bool isToggleable; @override Widget build(BuildContext context) { - return BlocBuilder( + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + ), + const VSpace(4), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), + maxLines: 3, + fontSize: 12, + ), + ], + ), + ), + IgnorePointer( + ignoring: !isToggleable, + child: Opacity( + opacity: isToggleable ? 1 : 0.5, + child: Toggle( + value: isEnabled, + onChanged: (_) => _onToggleChanged(context), + ), + ), + ), + ], + ); + } + + void _onToggleChanged(BuildContext context) { + if (isEnabled) { + showConfirmDialog( + context: context, + title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), + description: + LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () { + context + .read() + .add(const LocalAiPluginEvent.toggle()); + }, + ); + } else { + context.read().add(const LocalAiPluginEvent.toggle()); + } + } +} + +class LocalAISettingPanel extends StatelessWidget { + const LocalAISettingPanel({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( builder: (context, state) { - return state.pageIndicator.when( - error: (error) => SizedBox.shrink(), - loading: () => const SizedBox.shrink(), - isEnabled: (isEnabled) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), - ), - const Spacer(), - Toggle( - value: isEnabled, - onChanged: (_) { - if (isEnabled) { - showConfirmDialog( - context: context, - title: LocaleKeys - .settings_aiPage_keys_disableLocalAITitle - .tr(), - description: LocaleKeys - .settings_aiPage_keys_disableLocalAIDescription - .tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () => context - .read() - .add(const LocalAIToggleEvent.toggle()), - ); - } else { - context - .read() - .add(const LocalAIToggleEvent.toggle()); - } - }, - ), - ], - ), - const VSpace(4), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), - maxLines: 3, - fontSize: 12, - ), - ], - ); - }, + if (state is! ReadyLocalAiPluginState) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const LocalAIStatusIndicator(), + if (state.showSettings) ...[ + const VSpace(10), + OllamaSettingPage(), + ], + ], ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart deleted file mode 100644 index 5357db5c91..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'plugin_state.dart'; - -class LocalAISettingPanel extends StatelessWidget { - const LocalAISettingPanel({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LocalAISettingPanelBloc(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OllamaSettingPage(), - VSpace(6), - PluginStateIndicator(), - ], - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 05db831e04..861ef72432 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -20,25 +20,28 @@ class AIModelSelection extends StatelessWidget { buildWhen: (previous, current) => previous.availableModels != current.availableModels, builder: (context, state) { - if (state.availableModels == null) { + final models = state.availableModels?.models; + + if (models == null) { return const SizedBox( // Using same height as SettingsDropdown to avoid layout shift height: height, ); } + final localModels = models.where((model) => model.isLocal).toList(); + final cloudModels = models.where((model) => !model.isLocal).toList(); + return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( + Expanded( child: FlowyText.medium( LocaleKeys.settings_aiPage_keys_llmModelType.tr(), - fontSize: 14, + overflow: TextOverflow.ellipsis, ), ), - const Spacer(), Flexible( child: SettingsDropdown( key: const Key('_AIModelSelection'), @@ -46,12 +49,14 @@ class AIModelSelection extends StatelessWidget { .read() .add(SettingsAIEvent.selectModel(model)), selectedOption: state.availableModels!.selectedModel, - options: state.availableModels!.models + options: [...localModels, ...cloudModels] .map( (model) => buildDropdownMenuEntry( context, value: model, - label: model.i18n, + label: model.isLocal + ? "${model.i18n} (Local)" + : model.i18n, subLabel: model.desc, maximumHeight: height, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart new file mode 100644 index 0000000000..2abcb552d1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OllamaSettingPage extends StatelessWidget { + const OllamaSettingPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + OllamaSettingBloc()..add(const OllamaSettingEvent.started()), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.inputItems != current.inputItems || + previous.isEdited != current.isEdited, + builder: (context, state) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + padding: EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10, + children: [ + for (final item in state.inputItems) + _SettingItemWidget(item: item), + _SaveButton(isEdited: state.isEdited), + ], + ), + ); + }, + ), + ); + } +} + +class _SettingItemWidget extends StatelessWidget { + const _SettingItemWidget({required this.item}); + + final SettingItem item; + + @override + Widget build(BuildContext context) { + return Column( + key: ValueKey(item.content + item.settingType.title), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + item.settingType.title, + fontSize: 12, + figmaLineHeight: 16, + ), + const VSpace(4), + SizedBox( + height: 32, + child: FlowyTextField( + hintText: item.hintText, + text: item.content, + onChanged: (content) { + context.read().add( + OllamaSettingEvent.onEdit(content, item.settingType), + ); + }, + ), + ), + ], + ); + } +} + +class _SaveButton extends StatelessWidget { + const _SaveButton({required this.isEdited}); + + final bool isEdited; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerEnd, + child: FlowyTooltip( + message: isEdited ? null : 'No changes', + child: SizedBox( + child: FlowyButton( + text: FlowyText( + 'Apply', + figmaLineHeight: 20, + color: Theme.of(context).colorScheme.onPrimary, + ), + disable: !isEdited, + expandText: false, + margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), + backgroundColor: Theme.of(context).colorScheme.primary, + hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200), + onTap: () { + if (isEdited) { + context + .read() + .add(const OllamaSettingEvent.submit()); + } + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart deleted file mode 100644 index 8af4e35914..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class OllamaSettingPage extends StatelessWidget { - const OllamaSettingPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - OllamaSettingBloc()..add(const OllamaSettingEvent.started()), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.inputItems != current.inputItems || - previous.isEdited != current.isEdited, - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.inputItems.length, - separatorBuilder: (_, __) => const VSpace(10), - itemBuilder: (context, index) { - final item = state.inputItems[index]; - return _SettingItemWidget(item: item); - }, - ), - const VSpace(6), - Opacity( - opacity: 0.6, - child: _InstallOllamaInstruction(), - ), - _SaveButton(isEdited: state.isEdited), - ], - ); - }, - ), - ); - } -} - -class _SettingItemWidget extends StatelessWidget { - const _SettingItemWidget({required this.item}); - final SettingItem item; - - @override - Widget build(BuildContext context) { - return Column( - key: ValueKey(item.content + item.settingType.title), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText(item.settingType.title), - const VSpace(8), - FlowyTextField( - hintText: item.hintText, - text: item.content, - onChanged: (content) { - context.read().add( - OllamaSettingEvent.onEdit(content, item.settingType), - ); - }, - ), - ], - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({required this.isEdited}); - - final bool isEdited; - - @override - Widget build(BuildContext context) { - final tooltipMessage = isEdited ? 'Click to apply changes' : 'No changes'; - return SizedBox( - height: 50, - child: Row( - children: [ - const Spacer(), - SizedBox( - width: 120, - child: FlowyTooltip( - message: tooltipMessage, - child: Opacity( - opacity: isEdited ? 1 : 0.5, - child: FlowyTextButton( - 'Apply', - mainAxisAlignment: MainAxisAlignment.center, - onPressed: isEdited - ? () { - context - .read() - .add(const OllamaSettingEvent.submit()); - } - : null, - ), - ), - ), - ), - ], - ), - ); - } -} - -class _InstallOllamaInstruction extends StatelessWidget { - const _InstallOllamaInstruction(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - maxLines: 3, - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - ], - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart deleted file mode 100644 index 12ce391a29..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PluginStateIndicator extends StatelessWidget { - const PluginStateIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - PluginStateBloc()..add(const PluginStateEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return state.action.when( - unknown: () => const SizedBox.shrink(), - readToRun: () => const _PrepareRunning(), - initializingPlugin: () => const InitLocalAIIndicator(), - running: (version) => _LocalAIRunning( - key: ValueKey(version), - version: version, - ), - restartPlugin: () => const _RestartPluginButton(), - lackOfResource: (desc) => _LackOfResource(desc: desc), - ); - }, - ), - ); - } -} - -class _PrepareRunning extends StatelessWidget { - const _PrepareRunning(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - maxLines: 3, - ), - ), - ], - ); - } -} - -class _RestartPluginButton extends StatelessWidget { - const _RestartPluginButton(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const FlowySvg( - FlowySvgs.download_warn_s, - color: Color(0xFFC62828), - ), - const HSpace(6), - FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()), - const Spacer(), - SizedBox( - height: 30, - child: FlowyButton( - useIntrinsicWidth: true, - text: - FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()), - onTap: () { - context.read().add( - const PluginStateEvent.restartLocalAI(), - ); - }, - ), - ), - ], - ); - } -} - -class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning({required this.version, super.key}); - - final String version; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all( - Radius.circular(4), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - if (version.isNotEmpty) - Flexible( - child: FlowyText( - "($version) ", - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ), - Flexible( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _LackOfResource extends StatelessWidget { - const _LackOfResource({required this.desc}); - - final String desc; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - FlowySvgs.toast_warning_filled_s, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(6), - Expanded( - child: FlowyText( - desc, - maxLines: 3, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart new file mode 100644 index 0000000000..fdf03bb9ca --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart @@ -0,0 +1,359 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalAIStatusIndicator extends StatelessWidget { + const LocalAIStatusIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + ready: (_, version, runningState, lackOfResource) { + if (lackOfResource != null) { + return _LackOfResource(resource: lackOfResource); + } + + return switch (runningState) { + RunningStatePB.ReadyToRun => const _ReadyToRun(), + RunningStatePB.Connecting || + RunningStatePB.Connected => + _Initializing(), + RunningStatePB.Running => _LocalAIRunning(version: version), + RunningStatePB.Stopped => const _RestartPluginButton(), + _ => const SizedBox.shrink(), + }; + }, + orElse: () => const SizedBox.shrink(), + ); + }, + ); + } +} + +class _ReadyToRun extends StatelessWidget { + const _ReadyToRun(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const SizedBox.square( + dimension: 20.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + const HSpace(8.0), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStart.tr(), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + +class _Initializing extends StatelessWidget { + const _Initializing(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const SizedBox.square( + dimension: 20.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + HSpace(8), + Expanded( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + +class _RestartPluginButton extends StatelessWidget { + const _RestartPluginButton(); + + @override + Widget build(BuildContext context) { + final textStyle = + Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).isLightMode + ? const Color(0x80FFE7EE) + : const Color(0x80591734), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.toast_error_filled_s, + size: Size.square(20.0), + blendMode: null, + ), + const HSpace(8), + Expanded( + child: RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: + LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(), + style: textStyle?.copyWith( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + context + .read() + .add(const LocalAiPluginEvent.restart()); + }, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _LocalAIRunning extends StatelessWidget { + const _LocalAIRunning({ + required this.version, + }); + + final String version; + + @override + Widget build(BuildContext context) { + final runningText = LocaleKeys.settings_aiPage_keys_localAIRunning.tr(); + final text = version.isEmpty ? runningText : "$runningText ($version)"; + + return Container( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + Expanded( + child: FlowyText( + text, + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ), + ], + ), + ); + } +} + +class _LackOfResource extends StatelessWidget { + const _LackOfResource({required this.resource}); + + final LackOfAIResourcePB resource; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).isLightMode + ? const Color(0x80FFE7EE) + : const Color(0x80591734), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + FlowySvg( + FlowySvgs.toast_error_filled_s, + size: const Size.square(20.0), + blendMode: null, + ), + const HSpace(8), + Expanded( + child: switch (resource.resourceType) { + LackOfAIResourceTypePB.PluginExecutableNotReady => + _buildNoLAI(context), + LackOfAIResourceTypePB.OllamaServerNotReady => + _buildNoOllama(context), + LackOfAIResourceTypePB.MissingModel => + _buildNoModel(context, resource.missingModelNames), + _ => const SizedBox.shrink(), + }, + ), + ], + ), + ); + } + + TextStyle? _textStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); + } + + Widget _buildNoLAI(BuildContext context) { + final textStyle = _textStyle(context); + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(), + style: textStyle, + ), + TextSpan(text: ' ', style: _textStyle(context)), + ..._downloadInstructions(textStyle), + ], + ), + ); + } + + Widget _buildNoOllama(BuildContext context) { + final textStyle = _textStyle(context); + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(), + style: textStyle, + ), + TextSpan(text: ' ', style: textStyle), + ..._downloadInstructions(textStyle), + ], + ), + ); + } + + Widget _buildNoModel(BuildContext context, List modelNames) { + final textStyle = _textStyle(context); + + return RichText( + maxLines: 3, + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_instructions.tr(), + style: textStyle?.copyWith( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + afLaunchUrlString( + "https://appflowy.com/guide/appflowy-local-ai-ollama", + ); + }, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_downloadModel.tr(), + style: textStyle, + ), + ], + ), + ); + } + + List _downloadInstructions(TextStyle? textStyle) { + return [ + TextSpan( + text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), + style: textStyle, + ), + TextSpan( + text: ' ', + style: textStyle, + ), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_instructions.tr(), + style: textStyle?.copyWith( + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + afLaunchUrlString( + "https://appflowy.com/guide/appflowy-local-ai-ollama", + ); + }, + ), + TextSpan(text: ' ', style: textStyle), + TextSpan( + text: LocaleKeys.settings_aiPage_keys_installOllamaLai.tr(), + style: textStyle, + ), + ]; + } +} diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index e6a8be240c..6642644580 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -875,9 +875,9 @@ "activeOfflineAI": "نشط", "downloadOfflineAI": "التنزيل", "openModelDirectory": "افتح المجلد", - "localAISetupInstruction1": "اتبع هؤلاء", - "localAISetupInstruction2": "التعليمات", - "localAISetupInstruction3": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", + "pleaseFollowThese": "اتبع هؤلاء", + "instructions": "التعليمات", + "installOllamaLai": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", "startLocalAI": "قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي" } }, @@ -3217,4 +3217,4 @@ "rewrite": "إعادة كتابة", "insertBelow": "أدخل أدناه" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e93f5273e5..dcb39527c8 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -854,16 +854,16 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", + "localAIStart": "Local AI is starting. If it's slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading and may take a few seconds, depending on your device", "localAINotReadyRetryLater": "Local AI is initializing, please retry later", "localAIDisabled": "You are using local AI, but it is disabled. Please go to settings to enable it or try different model", + "localAIInitializing": "Local AI is loading. This may take a few minutes depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", - "failToLoadLocalAI": "Failed to start local AI", - "restartLocalAI": "Restart Local AI", + "failToLoadLocalAI": "Failed to start local AI.", + "restartLocalAI": "Restart", "disableLocalAITitle": "Disable local AI", "disableLocalAIDescription": "Do you want to disable local AI?", "localAIToggleTitle": "AppFlowy Local AI (LAI)", @@ -877,10 +877,13 @@ "activeOfflineAI": "Active", "downloadOfflineAI": "Download", "openModelDirectory": "Open folder", - "localAISetupInstruction1": "Follow these", - "localAISetupInstruction2": "instructions", - "localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI. Skip if you've already set it up", - "startLocalAI": "It may take a few seconds to start the local AI" + "laiNotReady": "The Local AI app was not installed correctly.", + "ollamaNotReady": "The Ollama server is not ready.", + "pleaseFollowThese": "Please follow these", + "instructions": "instructions", + "installOllamaLai": "to set up Ollama and AppFlowy Local AI.", + "modelsMissing": "Cannot find the required models.", + "downloadModel": "to download them." } }, "planPage": { @@ -3204,4 +3207,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 635efe92de..8e7192cf0b 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -868,9 +868,9 @@ "activeOfflineAI": "활성화됨", "downloadOfflineAI": "다운로드", "openModelDirectory": "폴더 열기", - "localAISetupInstruction1": "이 지침을 따르세요", - "localAISetupInstruction2": "지침", - "localAISetupInstruction3": "Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요", + "pleaseFollowThese": "지침", + "instructions": "이 지침을 따르세요", + "installOllamaLai": "Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요", "startLocalAI": "로컬 AI를 시작하는 데 몇 초가 소요될 수 있습니다" } }, @@ -3185,4 +3185,4 @@ "rewrite": "다시 작성", "insertBelow": "아래에 삽입" } -} \ No newline at end of file +} diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 30a3e5dd28..0075e35c04 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -586,7 +586,7 @@ pub struct LocalAIPB { pub enabled: bool, #[pb(index = 2, one_of)] - pub lack_of_resource: Option, + pub lack_of_resource: Option, #[pb(index = 3)] pub state: RunningStatePB, @@ -721,5 +721,35 @@ impl From for LocalAISetting { #[derive(Default, ProtoBuf, Clone, Debug)] pub struct LackOfAIResourcePB { #[pb(index = 1)] - pub resource_desc: String, + pub resource_type: LackOfAIResourceTypePB, + + #[pb(index = 2)] + pub missing_model_names: Vec, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum)] +pub enum LackOfAIResourceTypePB { + #[default] + PluginExecutableNotReady = 0, + OllamaServerNotReady = 1, + MissingModel = 2, +} + +impl From for LackOfAIResourcePB { + fn from(value: PendingResource) -> Self { + match value { + PendingResource::PluginExecutableNotReady => Self { + resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady, + missing_model_names: vec![], + }, + PendingResource::OllamaServerNotReady => Self { + resource_type: LackOfAIResourceTypePB::OllamaServerNotReady, + missing_model_names: vec![], + }, + PendingResource::MissingModel(model_name) => Self { + resource_type: LackOfAIResourceTypePB::MissingModel, + missing_model_names: vec![model_name], + }, + } + } } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 03b014c89d..b63401b829 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -1,5 +1,5 @@ use crate::ai_manager::AIUserService; -use crate::entities::{LackOfAIResourcePB, LocalAIPB, RunningStatePB}; +use crate::entities::{LocalAIPB, RunningStatePB}; use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, @@ -539,9 +539,7 @@ async fn initialize_ai_plugin( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB { - resource_desc: lack_of_resource, - }) + .payload(lack_of_resource) .send(); return Ok(()); diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 3eddcf2039..c40a9a9b88 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -47,22 +47,13 @@ pub enum WatchDiskEvent { Remove, } +#[derive(Debug, Clone)] pub enum PendingResource { PluginExecutableNotReady, OllamaServerNotReady, MissingModel(String), } -impl PendingResource { - pub fn desc(self) -> String { - match self { - PendingResource::PluginExecutableNotReady => "The Local AI app was not installed correctly. Please follow the instructions to install the Local AI application".to_string(), - PendingResource::OllamaServerNotReady => "Ollama is not ready. Please follow the instructions to install Ollama".to_string(), - PendingResource::MissingModel(model) => format!("Cannot find the model: {}. Please use the ollama pull command to install the model", model), - } - } -} - pub struct LocalAIResourceController { user_service: Arc, resource_service: Arc, @@ -128,10 +119,10 @@ impl LocalAIResourceController { return false; } - match self.calculate_pending_resources().await { - Ok(res) => res.is_empty(), - Err(_) => false, - } + self + .calculate_pending_resources() + .await + .is_ok_and(|r| r.is_none()) } pub async fn get_plugin_download_link(&self) -> FlowyResult { @@ -147,36 +138,35 @@ impl LocalAIResourceController { #[instrument(level = "info", skip_all, err)] pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { self.resource_service.store_setting(setting)?; - let mut resources = self.calculate_pending_resources().await?; - if let Some(resource) = resources.pop() { + if let Some(resource) = self.calculate_pending_resources().await? { chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB { - resource_desc: resource.desc(), - }) + .payload(LackOfAIResourcePB::from(resource)) .send(); } Ok(()) } - pub async fn get_lack_of_resource(&self) -> Option { - let mut resources = self.calculate_pending_resources().await.ok()?; - resources.pop().map(|r| r.desc()) + pub async fn get_lack_of_resource(&self) -> Option { + self + .calculate_pending_resources() + .await + .ok()? + .map(Into::into) } - pub async fn calculate_pending_resources(&self) -> FlowyResult> { - let mut resources = vec![]; + pub async fn calculate_pending_resources(&self) -> FlowyResult> { let app_path = ollama_plugin_path(); if !is_plugin_ready() { trace!("[LLM Resource] offline app not found: {:?}", app_path); - resources.push(PendingResource::PluginExecutableNotReady); - return Ok(resources); + return Ok(Some(PendingResource::PluginExecutableNotReady)); } let setting = self.get_llm_setting(); let client = Client::builder().timeout(Duration::from_secs(5)).build()?; + match client.get(&setting.ollama_server_url).send().await { Ok(resp) if resp.status().is_success() => { info!( @@ -189,8 +179,7 @@ impl LocalAIResourceController { "[LLM Resource] Ollama server is not responding at {}", setting.ollama_server_url ); - resources.push(PendingResource::OllamaServerNotReady); - return Ok(resources); + return Ok(Some(PendingResource::OllamaServerNotReady)); }, } @@ -201,12 +190,8 @@ impl LocalAIResourceController { match client.get(&tags_url).send().await { Ok(resp) if resp.status().is_success() => { - let tags: TagsResponse = resp.json().await.map_err(|e| { - log::error!( - "[LLM Resource] Failed to parse /api/tags JSON response: {:?}", - e - ); - e + let tags: TagsResponse = resp.json().await.inspect_err(|e| { + log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") })?; // Check each required model is present in the response. for required in &required_models { @@ -215,9 +200,7 @@ impl LocalAIResourceController { "[LLM Resource] required model '{}' not found in API response", required ); - resources.push(PendingResource::MissingModel(required.clone())); - // Optionally, you could continue checking all models rather than returning early. - return Ok(resources); + return Ok(Some(PendingResource::MissingModel(required.clone()))); } } }, @@ -226,12 +209,11 @@ impl LocalAIResourceController { "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", setting.ollama_server_url ); - resources.push(PendingResource::OllamaServerNotReady); - return Ok(resources); + return Ok(Some(PendingResource::OllamaServerNotReady)); }, } - Ok(resources) + Ok(None) } #[instrument(level = "info", skip_all)] From 7ed36f8736ef1a0f03969649212380e1d061e9b4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 31 Mar 2025 13:07:42 +0800 Subject: [PATCH 255/384] feat: support i18n in delete my account operation (#7635) --- .../settings/pages/account/account_deletion.dart | 7 ++++--- frontend/resources/translations/ar-SA.json | 4 ++-- frontend/resources/translations/de-DE.json | 4 ++-- frontend/resources/translations/en.json | 4 ++-- frontend/resources/translations/fr-FR.json | 4 ++-- frontend/resources/translations/ko-KR.json | 4 ++-- frontend/resources/translations/tr-TR.json | 4 ++-- frontend/resources/translations/vi-VN.json | 4 ++-- frontend/resources/translations/zh-CN.json | 9 ++++++++- 9 files changed, 26 insertions(+), 18 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 5b11e2d139..31139492ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -14,7 +14,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; -const _confirmText = 'DELETE MY ACCOUNT'; const _acceptableConfirmTexts = [ 'delete my account', 'deletemyaccount', @@ -135,7 +134,8 @@ class _AccountDeletionDialog extends StatelessWidget { ), const VSpace(12.0), FlowyTextField( - hintText: _confirmText, + hintText: + LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(), controller: controller, ), const VSpace(16), @@ -176,7 +176,8 @@ class _AccountDeletionDialog extends StatelessWidget { bool _isConfirmTextValid(String text) { // don't convert the text to lower case or upper case, // just check if the text is in the list - return _acceptableConfirmTexts.contains(text); + return _acceptableConfirmTexts.contains(text) || + text == LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(); } Future deleteMyAccount( diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 6642644580..bf7e9f4d6a 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -2610,12 +2610,12 @@ "dialogTitle": "حذف الحساب", "dialogContent1": "هل أنت متأكد أنك تريد حذف حسابك نهائياً؟", "dialogContent2": "لا يمكن التراجع عن هذا الإجراء، وسوف يؤدي إلى إزالة الوصول من جميع مساحات العمل، ومسح حسابك بالكامل، بما في ذلك مساحات العمل الخاصة، وإزالتك من جميع مساحات العمل المشتركة.", - "confirmHint1": "من فضلك اكتب \"حذف حسابي\" للتأكيد.", + "confirmHint1": "من فضلك اكتب \"@:newSettings.myAccount.deleteAccount.confirmHint3\" للتأكيد.", "confirmHint2": "أفهم أن هذا الإجراء لا رجعة فيه وسيؤدي إلى حذف حسابي وجميع البيانات المرتبطة به بشكل دائم.", "confirmHint3": "حذف حسابي", "checkToConfirmError": "يجب عليك تحديد المربع لتأكيد الحذف", "failedToGetCurrentUser": "فشل في الحصول على البريد الإلكتروني الحالي للمستخدم", - "confirmTextValidationFailed": "نص التأكيد الخاص بك لا يتطابق مع \"حذف حسابي\"", + "confirmTextValidationFailed": "نص التأكيد الخاص بك لا يتطابق مع \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", "deleteAccountSuccess": "تم حذف الحساب بنجاح" } }, diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 996ed03b7d..e95b40d26d 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -2517,11 +2517,11 @@ "dialogTitle": "Benutzerkonto löschen", "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst.", - "confirmHint1": "Geben Sie zur Bestätigung bitte „MEIN KONTO LÖSCHEN“ ein.", + "confirmHint1": "Geben Sie zur Bestätigung bitte „@:newSettings.myAccount.deleteAccount.confirmHint3“ ein.", "confirmHint3": "MEIN KONTO LÖSCHEN", "checkToConfirmError": "Sie müssen das Kontrollkästchen aktivieren, um das Löschen zu bestätigen", "failedToGetCurrentUser": "Aktuelle Benutzer-E-Mail konnte nicht abgerufen werden.", - "confirmTextValidationFailed": "Ihr Bestätigungstext stimmt nicht mit „MEIN KONTO LÖSCHEN“ überein.", + "confirmTextValidationFailed": "Ihr Bestätigungstext stimmt nicht mit „@:newSettings.myAccount.deleteAccount.confirmHint3“ überein.", "deleteAccountSuccess": "Konto erfolgreich gelöscht" } }, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index dcb39527c8..e0c735e2ff 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2599,12 +2599,12 @@ "dialogTitle": "Delete account", "dialogContent1": "Are you sure you want to permanently delete your account?", "dialogContent2": "This action cannot be undone, and will remove access from all workspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.", - "confirmHint1": "Please type \"DELETE MY ACCOUNT\" to confirm.", + "confirmHint1": "Please type \"@:newSettings.myAccount.deleteAccount.confirmHint3\" to confirm.", "confirmHint2": "I understand that this action is irreversible and will permanently delete my account and all associated data.", "confirmHint3": "DELETE MY ACCOUNT", "checkToConfirmError": "You must check the box to confirm deletion", "failedToGetCurrentUser": "Failed to get current user email", - "confirmTextValidationFailed": "Your confirmation text does not match \"DELETE MY ACCOUNT\"", + "confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", "deleteAccountSuccess": "Account deleted successfully" } }, diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 270b92d72f..0d542e916b 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -2519,12 +2519,12 @@ "dialogTitle": "Supprimer le compte", "dialogContent1": "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?", "dialogContent2": "Cette action ne peut pas être annulée et supprimera l'accès à tous les espaces d'équipe, effaçant l'intégralité de votre compte, y compris les espaces de travail privés, et vous supprimant de tous les espaces de travail partagés.", - "confirmHint1": "Veuillez taper « SUPPRIMER MON COMPTE » pour confirmer.", + "confirmHint1": "Veuillez taper « @:newSettings.myAccount.deleteAccount.confirmHint3 » pour confirmer.", "confirmHint2": "Je comprends que cette action est irréversible et supprimera définitivement mon compte et toutes les données associées.", "confirmHint3": "SUPPRIMER MON COMPTE", "checkToConfirmError": "Vous devez cocher la case pour confirmer la suppression", "failedToGetCurrentUser": "Impossible d'obtenir l'e-mail de l'utilisateur actuel", - "confirmTextValidationFailed": "Votre texte de confirmation ne correspond pas à « SUPPRIMER MON COMPTE »", + "confirmTextValidationFailed": "Votre texte de confirmation ne correspond pas à « @:newSettings.myAccount.deleteAccount.confirmHint3 »", "deleteAccountSuccess": "Compte supprimé avec succès" } }, diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 8e7192cf0b..66240d954a 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -2579,12 +2579,12 @@ "dialogTitle": "계정 삭제", "dialogContent1": "계정을 영구적으로 삭제하시겠습니까?", "dialogContent2": "이 작업은 되돌릴 수 없으며, 모든 작업 공간에서 액세스를 제거하고, 개인 작업 공간을 포함한 전체 계정을 삭제하며, 모든 공유 작업 공간에서 제거됩니다.", - "confirmHint1": "\"내 계정 삭제\"를 입력하여 확인하세요.", + "confirmHint1": "\"@:newSettings.myAccount.deleteAccount.confirmHint3\"를 입력하여 확인하세요.", "confirmHint2": "이 작업은 되돌릴 수 없으며, 계정과 모든 관련 데이터를 영구적으로 삭제합니다.", "confirmHint3": "내 계정 삭제", "checkToConfirmError": "삭제를 확인하려면 확인란을 선택해야 합니다", "failedToGetCurrentUser": "현재 사용자 이메일을 가져오지 못했습니다", - "confirmTextValidationFailed": "확인 텍스트가 \"내 계정 삭제\"와 일치하지 않습니다", + "confirmTextValidationFailed": "확인 텍스트가 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"와 일치하지 않습니다", "deleteAccountSuccess": "계정이 성공적으로 삭제되었습니다" } }, diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index d23566e512..346742f295 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -2518,12 +2518,12 @@ "dialogTitle": "Hesabı sil", "dialogContent1": "Hesabınızı kalıcı olarak silmek istediğinizden emin misiniz?", "dialogContent2": "Bu işlem GERİ ALINAMAZ ve tüm çalışma alanlarından erişiminizi kaldıracak, özel çalışma alanları dahil tüm hesabınızı silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır.", - "confirmHint1": "Onaylamak için lütfen \"HESABIMI SİL\" yazın.", + "confirmHint1": "Onaylamak için lütfen \"@:newSettings.myAccount.deleteAccount.confirmHint3\" yazın.", "confirmHint2": "Bu işlemin GERİ ALINAMAZ olduğunu ve hesabımı ve ilişkili tüm verileri kalıcı olarak sileceğini anlıyorum.", "confirmHint3": "HESABIMI SİL", "checkToConfirmError": "Silme işlemini onaylamak için kutuyu işaretlemelisiniz", "failedToGetCurrentUser": "Mevcut kullanıcı e-postası alınamadı", - "confirmTextValidationFailed": "Onay metniniz \"HESABIMI SİL\" ile eşleşmiyor", + "confirmTextValidationFailed": "Onay metniniz \"@:newSettings.myAccount.deleteAccount.confirmHint3\" ile eşleşmiyor", "deleteAccountSuccess": "Hesap başarıyla silindi" } }, diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index a506a84db3..7e27e8a648 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -2270,12 +2270,12 @@ "dialogTitle": "Xóa tài khoản", "dialogContent1": "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không?", "dialogContent2": "Không thể hoàn tác hành động này và sẽ xóa quyền truy cập khỏi mọi không gian làm việc nhóm, xóa toàn bộ tài khoản của bạn, bao gồm cả không gian làm việc riêng tư và xóa bạn khỏi mọi không gian làm việc được chia sẻ.", - "confirmHint1": "Vui lòng nhập \"XÓA TÀI KHOẢN CỦA TÔI\" để xác nhận.", + "confirmHint1": "Vui lòng nhập \"@:newSettings.myAccount.deleteAccount.confirmHint3\" để xác nhận.", "confirmHint2": "Tôi hiểu rằng hành động này là không thể đảo ngược và sẽ xóa vĩnh viễn tài khoản của tôi cùng mọi dữ liệu liên quan.", "confirmHint3": "XÓA TÀI KHOẢN CỦA TÔI", "checkToConfirmError": "Bạn phải đánh dấu vào ô để xác nhận việc xóa", "failedToGetCurrentUser": "Không lấy được email người dùng hiện tại", - "confirmTextValidationFailed": "Văn bản xác nhận của bạn không khớp với \"XÓA TÀI KHOẢN CỦA TÔI\"", + "confirmTextValidationFailed": "Văn bản xác nhận của bạn không khớp với \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", "deleteAccountSuccess": "Tài khoản đã được xóa thành công" } }, diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index ce0c760f3d..557916b5b5 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1920,7 +1920,14 @@ "deleteMyAccount": "删除我的账户", "dialogTitle": "删除帐户", "dialogContent1": "你确定要永久删除您的帐户吗?", - "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。" + "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。", + "confirmHint1": "请输入 \"@:newSettings.myAccount.deleteAccount.confirmHint3\" 以确认。", + "confirmHint2": "我理解此操作是不可逆的,并且将永久删除我的帐户和所有关联数据。", + "confirmHint3": "删除我的账户", + "checkToConfirmError": "你必须勾选以确认删除。", + "failedToGetCurrentUser": "获取当前用户邮箱失败", + "confirmTextValidationFailed": "你的确认文本不匹配 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", + "deleteAccountSuccess": "账户删除成功" } }, "workplace": { From 0b298ea426ca15c9d12c7a6c4b6ca597b9c4df30 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:06:39 +0800 Subject: [PATCH 256/384] chore: use emoji instead of Local for copy (#7656) --- .../settings/pages/setting_ai_view/model_selection.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 861ef72432..30ff4addde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -54,9 +54,8 @@ class AIModelSelection extends StatelessWidget { (model) => buildDropdownMenuEntry( context, value: model, - label: model.isLocal - ? "${model.i18n} (Local)" - : model.i18n, + label: + model.isLocal ? "${model.i18n} 🔐" : model.i18n, subLabel: model.desc, maximumHeight: height, ), From 3c99105b23bad0c083a938154cab49bf98beb55d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:06:50 +0800 Subject: [PATCH 257/384] fix: show cursor at end of selection when generating (#7655) --- .../ai/widgets/ai_writer_scroll_wrapper.dart | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 90bce596a0..6b5f27e028 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -208,11 +208,26 @@ class _AiWriterScrollWrapperState extends State { throttler.call(() { if (aiWriterCubit.aiWriterNode != null) { final path = aiWriterCubit.aiWriterNode!.path; - if (path.isNotEmpty) { - widget.editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); + + if (path.isEmpty) { + return; } + + if (path.previous.isNotEmpty) { + final node = widget.editorState.getNodeAtPath(path.previous); + if (node != null && node.delta != null && node.delta!.isNotEmpty) { + widget.editorState.updateSelectionWithReason( + Selection.collapsed( + Position(path: path, offset: node.delta!.length), + ), + ); + return; + } + } + + widget.editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: path)), + ); } }); } From 5e1a8b1ec74c27bd8471d26c82ae911afb7cfd6c Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 31 Mar 2025 15:16:56 +0800 Subject: [PATCH 258/384] fix: toolbar link launch review issues (#7639) * fix: some toolbar link launch review issues * fix: support check link format for link menus * fix: toolbar and link hover menu will not display together * fix: filter link search result with current document id * fix: remove error text while link menu is not focus * fix: some issues * fix: test errors * fix: add tooltip for link menu --- .../document/presentation/editor_page.dart | 76 +++--- .../desktop_floating_toolbar.dart | 62 ++++- .../link/link_create_menu.dart | 89 +++++-- .../desktop_toolbar/link/link_edit_menu.dart | 235 +++++++++++------- .../desktop_toolbar/link/link_hover_menu.dart | 162 +++++++++--- .../link/link_search_text_field.dart | 60 +++-- .../desktop_toolbar/link/link_styles.dart | 24 +- .../desktop_toolbar/toolbar_cubit.dart | 17 -- .../custom_format_toolbar_items.dart | 3 +- .../custom_hightlight_color_toolbar_item.dart | 3 +- .../custom_link_toolbar_item.dart | 26 +- .../custom_text_color_toolbar_item.dart | 3 +- .../more_option_toolbar_item.dart | 12 +- .../appflowy_flutter/lib/startup/startup.dart | 4 + 14 files changed, 529 insertions(+), 247 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index a6cff2ae07..ee3a852cc1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -432,44 +432,50 @@ class _AppFlowyEditorPageState extends State ); } return Center( - child: FloatingToolbar( - floatingToolbarHeight: 40, - padding: EdgeInsets.symmetric(horizontal: 6), - style: FloatingToolbarStyle( - backgroundColor: Theme.of(context).cardColor, - toolbarActiveColor: Color(0xffe0f8fd), - ), - items: toolbarItems, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 24, - color: themeV2.shadow_medium, + child: BlocProvider.value( + value: context.read(), + child: FloatingToolbar( + floatingToolbarHeight: 40, + padding: EdgeInsets.symmetric(horizontal: 6), + style: FloatingToolbarStyle( + backgroundColor: Theme.of(context).cardColor, + toolbarActiveColor: Color(0xffe0f8fd), + ), + items: toolbarItems, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 24, + color: themeV2.shadow_medium, + ), + ], + ), + toolbarBuilder: (_, child, onDismiss, isMetricsChanged) => + BlocProvider.value( + value: context.read(), + child: DesktopFloatingToolbar( + editorState: editorState, + onDismiss: onDismiss, + enableAnimation: false, + child: child, ), - ], - ), - toolbarBuilder: (context, child, onDismiss, isMetricsChanged) => - DesktopFloatingToolbar( + ), + placeHolderBuilder: (_) => customPlaceholderItem, editorState: editorState, - onDismiss: onDismiss, - enableAnimation: !isMetricsChanged, - child: child, + editorScrollController: editorScrollController, + textDirection: textDirection, + tooltipBuilder: (context, id, message, child) => + widget.styleCustomizer.buildToolbarItemTooltip( + context, + id, + message, + child, + ), + child: editor, ), - placeHolderBuilder: (_) => customPlaceholderItem, - editorState: editorState, - editorScrollController: editorScrollController, - textDirection: textDirection, - tooltipBuilder: (context, id, message, child) => - widget.styleCustomizer.buildToolbarItemTooltip( - context, - id, - message, - child, - ), - child: editor, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart index daa066d50c..03fc12a37c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart @@ -1,10 +1,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'toolbar_animation.dart'; -import 'toolbar_cubit.dart'; class DesktopFloatingToolbar extends StatefulWidget { const DesktopFloatingToolbar({ @@ -28,6 +27,7 @@ class _DesktopFloatingToolbarState extends State { EditorState get editorState => widget.editorState; _Position? position; + final toolbarController = getIt(); @override void initState() { @@ -39,24 +39,32 @@ class _DesktopFloatingToolbarState extends State { final selectionRect = editorState.selectionRects(); if (selectionRect.isEmpty) return; position = calculateSelectionMenuOffset(selectionRect.first); + toolbarController._addCallback(dismiss); + } + + @override + void dispose() { + toolbarController._removeCallback(dismiss); + super.dispose(); } @override Widget build(BuildContext context) { if (position == null) return Container(); - return BlocProvider( - create: (_) => ToolbarCubit(widget.onDismiss), - child: Positioned( - left: position!.left, - top: position!.top, - right: position!.right, - child: widget.enableAnimation - ? ToolbarAnimationWidget(child: widget.child) - : widget.child, - ), + return Positioned( + left: position!.left, + top: position!.top, + right: position!.right, + child: widget.enableAnimation + ? ToolbarAnimationWidget(child: widget.child) + : widget.child, ); } + void dismiss() { + widget.onDismiss.call(); + } + _Position calculateSelectionMenuOffset( Rect rect, ) { @@ -92,3 +100,33 @@ class _Position { final double? top; final double? right; } + +class FloatingToolbarController { + final Set _dismissCallbacks = {}; + final Set _displayListeners = {}; + + void _addCallback(VoidCallback callback) { + _dismissCallbacks.add(callback); + for (final listener in Set.of(_displayListeners)) { + listener.call(); + } + } + + void _removeCallback(VoidCallback callback) => + _dismissCallbacks.remove(callback); + + bool get isToolbarShowing => _dismissCallbacks.isNotEmpty; + + void addDisplayListener(VoidCallback listener) => + _displayListeners.add(listener); + + void removeDisplayListener(VoidCallback listener) => + _displayListeners.remove(listener); + + void hideToolbar() { + if (_dismissCallbacks.isEmpty) return; + for (final callback in _dismissCallbacks) { + callback.call(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index 2f5edc195d..bc14073193 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -19,11 +19,15 @@ class LinkCreateMenu extends StatefulWidget { required this.onSubmitted, required this.onDismiss, required this.alignment, + required this.currentViewId, + required this.initialText, }); final EditorState editorState; final void Function(String link, bool isPage) onSubmitted; final VoidCallback onDismiss; + final String currentViewId; + final String initialText; final LinkMenuAlignment alignment; @override @@ -32,6 +36,8 @@ class LinkCreateMenu extends StatefulWidget { class _LinkCreateMenuState extends State { late LinkSearchTextField searchTextField = LinkSearchTextField( + currentViewId: widget.currentViewId, + initialSearchText: widget.initialText, onEnter: () { searchTextField.onSearchResult( onLink: () => onSubmittedLink(), @@ -48,17 +54,28 @@ class _LinkCreateMenuState extends State { }, ); - bool get isButtonEnable => searchText.isNotEmpty; + bool get isTextfieldEnable => searchTextField.isTextfieldEnable; String get searchText => searchTextField.searchText; bool get showAtTop => widget.alignment.isTop; + bool showErrorText = false; + @override void initState() { super.initState(); searchTextField.requestFocus(); searchTextField.searchRecentViews(); + final focusNode = searchTextField.focusNode; + bool hasFocus = focusNode.hasFocus; + focusNode.addListener(() { + if (hasFocus != focusNode.hasFocus && mounted) { + setState(() { + hasFocus = focusNode.hasFocus; + }); + } + }); } @override @@ -98,32 +115,45 @@ class _LinkCreateMenuState extends State { Widget buildSearchContainer() { return Container( width: 320, - height: 48, decoration: buildToolbarLinkDecoration(context), padding: EdgeInsets.all(8), child: ValueListenableBuilder( valueListenable: searchTextField.textEditingController, builder: (context, _, __) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: searchTextField.buildTextField()), - HSpace(8), - FlowyTextButton( - LocaleKeys.document_toolbar_insert.tr(), - mainAxisAlignment: MainAxisAlignment.center, - padding: EdgeInsets.zero, - constraints: BoxConstraints(maxWidth: 72, minHeight: 32), - fontSize: 14, - fontColor: - isButtonEnable ? Colors.white : LinkStyle.textTertiary, - fillColor: isButtonEnable - ? LinkStyle.fillThemeThick - : LinkStyle.borderColor, - hoverColor: LinkStyle.fillThemeThick, - lineHeight: 20 / 14, - fontWeight: FontWeight.w600, - onPressed: isButtonEnable ? () => onSubmittedLink() : null, + Row( + children: [ + Expanded( + child: searchTextField.buildTextField(context: context), + ), + HSpace(8), + FlowyTextButton( + LocaleKeys.document_toolbar_insert.tr(), + mainAxisAlignment: MainAxisAlignment.center, + padding: EdgeInsets.zero, + constraints: BoxConstraints(maxWidth: 72, minHeight: 32), + fontSize: 14, + fontColor: Colors.white, + fillColor: LinkStyle.fillThemeThick, + hoverColor: LinkStyle.fillThemeThick.withAlpha(200), + lineHeight: 20 / 14, + fontWeight: FontWeight.w600, + onPressed: onSubmittedLink, + ), + ], ), + if (showErrorText) + Padding( + padding: const EdgeInsets.only(top: 4), + child: FlowyText.regular( + LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), + color: LinkStyle.textStatusError, + fontSize: 12, + figmaLineHeight: 16, + ), + ), ], ); }, @@ -131,7 +161,15 @@ class _LinkCreateMenuState extends State { ); } - void onSubmittedLink() => widget.onSubmitted(searchText, false); + void onSubmittedLink() { + if (!isTextfieldEnable) { + setState(() { + showErrorText = true; + }); + return; + } + widget.onSubmitted(searchText, false); + } void onSubmittedPageLink(ViewPB view) async { final workspaceId = context @@ -152,13 +190,16 @@ void showLinkCreateMenu( BuildContext context, EditorState editorState, Selection selection, + String currentViewId, ) { + if (!context.mounted) return; final (left, top, right, bottom, alignment) = _getPosition(editorState); final node = editorState.getNodeAtPath(selection.end.path); if (node == null) { return; } + final selectedText = editorState.getTextInSelection(selection).join(); OverlayEntry? overlay; @@ -178,12 +219,18 @@ void showLinkCreateMenu( builder: (context) { return LinkCreateMenu( alignment: alignment, + initialText: selectedText, + currentViewId: currentViewId, editorState: editorState, onSubmitted: (link, isPage) async { await editorState.formatDelta(selection, { BuiltInAttributeKey.href: link, kIsPageLink: isPage, }); + await editorState.updateSelectionWithReason( + null, + reason: SelectionUpdateReason.uiEvent, + ); dismissOverlay(); }, onDismiss: dismissOverlay, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart index e10b46411b..b6a4ad89ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -10,7 +11,8 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; - +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/util/link_util.dart'; import 'link_create_menu.dart'; import 'link_search_text_field.dart'; import 'link_styles.dart'; @@ -22,12 +24,14 @@ class LinkEditMenu extends StatefulWidget { required this.onDismiss, required this.onApply, required this.onRemoveLink, + required this.currentViewId, }); final LinkInfo linkInfo; final ValueChanged onApply; - final VoidCallback onRemoveLink; + final ValueChanged onRemoveLink; final VoidCallback onDismiss; + final String currentViewId; @override State createState() => _LinkEditMenuState(); @@ -36,7 +40,7 @@ class LinkEditMenu extends StatefulWidget { class _LinkEditMenuState extends State { ValueChanged get onApply => widget.onApply; - VoidCallback get onRemoveLink => widget.onRemoveLink; + ValueChanged get onRemoveLink => widget.onRemoveLink; VoidCallback get onDismiss => widget.onDismiss; @@ -47,9 +51,7 @@ class _LinkEditMenuState extends State { late LinkSearchTextField searchTextField; bool isShowingSearchResult = false; ViewPB? currentView; - - bool get enableApply => - linkInfo.link.isNotEmpty && linkNameController.text.isNotEmpty; + bool showErrorText = false; @override void initState() { @@ -58,18 +60,9 @@ class _LinkEditMenuState extends State { if (isPageLink) getPageView(); searchTextField = LinkSearchTextField( initialSearchText: isPageLink ? '' : linkInfo.link, - onEnter: () { - searchTextField.onSearchResult( - onLink: onLinkSelected, - onRecentViews: () => - onPageSelected(searchTextField.currentRecentView), - onSearchViews: () => - onPageSelected(searchTextField.currentSearchedView), - onEmpty: () { - searchTextField.unfocus(); - }, - ); - }, + initialViewId: linkInfo.viewId, + currentViewId: widget.currentViewId, + onEnter: onConfirm, onEscape: () { if (isShowingSearchResult) { hideSearchResult(); @@ -95,6 +88,7 @@ class _LinkEditMenuState extends State { Widget build(BuildContext context) { final showingRecent = searchTextField.showingRecent && isShowingSearchResult; + final errorHeight = showErrorText ? 20.0 : 0.0; return GestureDetector( onTap: onDismiss, child: Container( @@ -107,9 +101,8 @@ class _LinkEditMenuState extends State { onTap: hideSearchResult, child: Container( width: 400, - height: 192, + height: 192 + errorHeight, decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.fromLTRB(20, 16, 20, 16), ), ), Positioned( @@ -123,7 +116,7 @@ class _LinkEditMenuState extends State { ), ), Positioned( - top: 80, + top: 80 + errorHeight, left: 20, child: FlowyText.semibold( LocaleKeys.document_toolbar_linkName.tr(), @@ -133,12 +126,12 @@ class _LinkEditMenuState extends State { ), ), Positioned( - top: 152, + top: 144 + errorHeight, left: 20, child: buildButtons(), ), Positioned( - top: 108, + top: 100 + errorHeight, left: 20, child: buildNameTextField(), ), @@ -155,29 +148,53 @@ class _LinkEditMenuState extends State { Widget buildLinkField() { final showPageView = linkInfo.isPage && !isShowingSearchResult; - if (showPageView) return buildPageView(); - if (!isShowingSearchResult) return buildLinkView(); - return SizedBox( - width: 360, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 360, - height: 32, - child: searchTextField.buildTextField( - autofocus: true, + Widget child; + if (showPageView) { + child = buildPageView(); + } else if (!isShowingSearchResult) { + child = buildLinkView(); + } else { + return SizedBox( + width: 360, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 360, + height: 32, + child: searchTextField.buildTextField( + autofocus: true, + context: context, + ), + ), + VSpace(6), + searchTextField.buildResultContainer( + context: context, + width: 360, + onPageLinkSelected: onPageSelected, + onLinkSelected: onLinkSelected, + ), + ], + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + child, + if (showErrorText) + Padding( + padding: const EdgeInsets.only(top: 4), + child: FlowyText.regular( + LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), + color: LinkStyle.textStatusError, + fontSize: 12, + figmaLineHeight: 16, ), ), - VSpace(6), - searchTextField.buildResultContainer( - context: context, - width: 360, - onPageLinkSelected: onPageSelected, - onLinkSelected: onLinkSelected, - ), - ], - ), + ], ); } @@ -197,15 +214,15 @@ class _LinkEditMenuState extends State { preferBelow: false, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor), + border: Border.all(color: LinkStyle.borderColor(context)), ), - onPressed: onRemoveLink, + onPressed: () => onRemoveLink.call(linkInfo), ), Spacer(), DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor), + border: Border.all(color: LinkStyle.borderColor(context)), ), child: FlowyTextButton( LocaleKeys.button_cancel.tr(), @@ -214,7 +231,9 @@ class _LinkEditMenuState extends State { constraints: BoxConstraints(maxWidth: 78, minHeight: 32), fontSize: 14, lineHeight: 20 / 14, - fontColor: LinkStyle.textPrimary, + fontColor: Theme.of(context).isLightMode + ? LinkStyle.textPrimary + : Theme.of(context).iconTheme.color, fillColor: Colors.transparent, fontWeight: FontWeight.w400, onPressed: onDismiss, @@ -232,14 +251,26 @@ class _LinkEditMenuState extends State { fontSize: 14, lineHeight: 20 / 14, hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - fontColor: - enableApply ? Colors.white : LinkStyle.textTertiary, - fillColor: enableApply - ? LinkStyle.fillThemeThick - : LinkStyle.borderColor, + fontColor: Colors.white, + fillColor: LinkStyle.fillThemeThick, fontWeight: FontWeight.w400, - onPressed: - enableApply ? () => widget.onApply.call(linkInfo) : null, + onPressed: () { + if (isShowingSearchResult) { + onConfirm(); + return; + } + if (linkInfo.link.isEmpty) { + widget.onRemoveLink(linkInfo); + return; + } + if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) { + setState(() { + showErrorText = true; + }); + return; + } + widget.onApply.call(linkInfo); + }, ); }, ), @@ -272,6 +303,7 @@ class _LinkEditMenuState extends State { }, decoration: LinkStyle.buildLinkTextFieldInputDecoration( LocaleKeys.document_toolbar_linkNameHint.tr(), + context, ), ), ); @@ -288,24 +320,34 @@ class _LinkEditMenuState extends State { ), ); } else { + final viewName = view.name; + final displayName = viewName.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : viewName; child = GestureDetector( onTap: showSearchResult, child: MouseRegion( cursor: SystemMouseCursors.click, - child: Container( - height: 32, - padding: EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Row( - children: [ - searchTextField.buildIcon(view), - HSpace(8), - Flexible( - child: FlowyText.regular( - view.name, - overflow: TextOverflow.ellipsis, + child: FlowyTooltip( + preferBelow: false, + message: displayName, + child: Container( + height: 32, + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Row( + children: [ + searchTextField.buildIcon(view), + HSpace(4), + Flexible( + child: FlowyText.regular( + displayName, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 20, + fontSize: 14, + ), ), - ), - ], + ], + ), ), ), ), @@ -324,24 +366,28 @@ class _LinkEditMenuState extends State { width: 360, height: 32, decoration: buildDecoration(), - child: GestureDetector( - onTap: showSearchResult, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Row( - children: [ - FlowySvg(FlowySvgs.toolbar_link_earth_m), - HSpace(8), - Flexible( - child: FlowyText.regular( - linkInfo.link, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, + child: FlowyTooltip( + preferBelow: false, + message: linkInfo.link, + child: GestureDetector( + onTap: showSearchResult, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: EdgeInsets.fromLTRB(8, 6, 8, 6), + child: Row( + children: [ + FlowySvg(FlowySvgs.toolbar_link_earth_m), + HSpace(8), + Flexible( + child: FlowyText.regular( + linkInfo.link, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 20, + ), ), - ), - ], + ], + ), ), ), ), @@ -349,12 +395,21 @@ class _LinkEditMenuState extends State { ); } + void onConfirm() { + searchTextField.onSearchResult( + onLink: onLinkSelected, + onRecentViews: () => onPageSelected(searchTextField.currentRecentView), + onSearchViews: () => onPageSelected(searchTextField.currentSearchedView), + onEmpty: () { + searchTextField.unfocus(); + }, + ); + } + Future getPageView() async { if (!linkInfo.isPage) return; - final link = linkInfo.link; - final viewId = link.split('/').lastOrNull ?? ''; final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(viewId); + await ViewBackendService.getMentionPageStatus(linkInfo.viewId); if (mounted) { setState(() { currentView = view; @@ -413,7 +468,7 @@ class _LinkEditMenuState extends State { BoxDecoration buildDecoration() => BoxDecoration( borderRadius: BorderRadius.circular(8), - border: Border.all(color: LinkStyle.borderColor), + border: Border.all(color: LinkStyle.borderColor(context)), ); } @@ -426,4 +481,6 @@ class LinkInfo { Attributes toAttribute() => {AppFlowyRichTextKeys.href: link, kIsPageLink: isPage}; + + String get viewId => isPage ? link.split('/').lastOrNull ?? '' : ''; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index e47386699b..b07d2949d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -3,20 +3,25 @@ import 'dart:math'; import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'link_create_menu.dart'; import 'link_edit_menu.dart'; -import 'link_styles.dart'; class LinkHoverTrigger extends StatefulWidget { const LinkHoverTrigger({ @@ -45,6 +50,7 @@ class LinkHoverTrigger extends StatefulWidget { class _LinkHoverTriggerState extends State { final hoverMenuController = PopoverController(); final editMenuController = PopoverController(); + final toolbarController = getIt(); bool isHoverMenuShowing = false; bool isHoverMenuHovering = false; bool isHoverTriggerHovering = false; @@ -57,12 +63,13 @@ class _LinkHoverTriggerState extends State { Attributes get attribute => widget.attribute; - HoverTriggerKey get triggerKey => HoverTriggerKey(widget.node.id, selection); + late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection); @override void initState() { super.initState(); getIt()._add(triggerKey, showLinkHoverMenu); + toolbarController.addDisplayListener(onToolbarShow); } @override @@ -70,6 +77,7 @@ class _LinkHoverTriggerState extends State { hoverMenuController.close(); editMenuController.close(); getIt()._remove(triggerKey, showLinkHoverMenu); + toolbarController.removeDisplayListener(onToolbarShow); super.dispose(); } @@ -132,9 +140,9 @@ class _LinkHoverTriggerState extends State { tryToDismissLinkHoverMenu(); }, onOpenLink: openLink, - onCopyLink: copyLink, + onCopyLink: () => copyLink(context), onEditLink: showLinkEditMenu, - onRemoveLink: () => removeLink(editorState, selection, true), + onRemoveLink: () => removeLink(editorState, selection), ), child: child, ); @@ -144,6 +152,7 @@ class _LinkHoverTriggerState extends State { final href = attribute.href ?? '', isPage = attribute.isPage, title = editorState.getTextInSelection(selection).join(); + final currentViewId = context.read()?.documentId ?? ''; return AppFlowyPopover( controller: editMenuController, direction: PopoverDirection.bottomWithLeftAligned, @@ -159,6 +168,7 @@ class _LinkHoverTriggerState extends State { minHeight: 282, ), popupBuilder: (context) => LinkEditMenu( + currentViewId: currentViewId, linkInfo: LinkInfo(name: title, link: href, isPage: isPage), onDismiss: () => editMenuController.close(), onApply: (info) async { @@ -173,14 +183,19 @@ class _LinkHoverTriggerState extends State { editMenuController.close(); await editorState.apply(transaction); }, - onRemoveLink: () => removeLink(editorState, selection, true), + onRemoveLink: (linkinfo) => + onRemoveAndReplaceLink(editorState, selection, linkinfo.name), ), child: child, ); } + void onToolbarShow() => hoverMenuController.close(); + void showLinkHoverMenu() { - if (isHoverMenuShowing) return; + if (isHoverMenuShowing || toolbarController.isToolbarShowing || !mounted) { + return; + } keepEditorFocusNotifier.increase(); hoverMenuController.show(); } @@ -219,20 +234,24 @@ class _LinkHoverTriggerState extends State { } } - Future copyLink() async { + Future copyLink(BuildContext context) async { final href = widget.attribute.href ?? ''; if (href.isEmpty) return; await getIt() .setData(ClipboardServiceData(plainText: href)); + if (context.mounted) { + showToastNotification( + context, + message: LocaleKeys.shareAction_copyLinkSuccess.tr(), + ); + } hoverMenuController.close(); } void removeLink( EditorState editorState, Selection selection, - bool isHref, ) { - if (!isHref) return; final node = editorState.getNodeAtPath(selection.end.path); if (node == null) { return; @@ -251,9 +270,34 @@ class _LinkHoverTriggerState extends State { ); editorState.apply(transaction); } + + void onRemoveAndReplaceLink( + EditorState editorState, + Selection selection, + String text, + ) { + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final index = selection.normalized.startIndex; + final length = selection.length; + final transaction = editorState.transaction + ..replaceText( + node, + index, + length, + text, + attributes: { + BuiltInAttributeKey.href: null, + kIsPageLink: null, + }, + ); + editorState.apply(transaction); + } } -class LinkHoverMenu extends StatelessWidget { +class LinkHoverMenu extends StatefulWidget { const LinkHoverMenu({ super.key, required this.attribute, @@ -275,17 +319,31 @@ class LinkHoverMenu extends StatelessWidget { final VoidCallback onEditLink; final VoidCallback onRemoveLink; + @override + State createState() => _LinkHoverMenuState(); +} + +class _LinkHoverMenuState extends State { + ViewPB? currentView; + late bool isPage = widget.attribute.isPage; + late String href = widget.attribute.href ?? ''; + + @override + void initState() { + super.initState(); + if (isPage) getPageView(); + } + @override Widget build(BuildContext context) { - final href = attribute.href ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MouseRegion( - onEnter: onEnter, - onExit: onExit, + onEnter: widget.onEnter, + onExit: widget.onExit, child: SizedBox( - width: max(320, triggerSize.width), + width: max(320, widget.triggerSize.width), height: 48, child: Align( alignment: Alignment.centerLeft, @@ -293,21 +351,15 @@ class LinkHoverMenu extends StatelessWidget { width: 320, height: 48, decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.all(8), + padding: EdgeInsets.fromLTRB(12, 8, 8, 8), child: Row( children: [ - Expanded( - child: FlowyText.regular( - href, - fontSize: 14, - figmaLineHeight: 20, - overflow: TextOverflow.ellipsis, - ), - ), + Expanded(child: buildLinkWidget()), Container( height: 20, width: 1, - color: LinkStyle.borderColor, + color: Color(0xffE8ECF3) + .withAlpha(Theme.of(context).isLightMode ? 255 : 40), margin: EdgeInsets.symmetric(horizontal: 6), ), FlowyIconButton( @@ -315,21 +367,21 @@ class LinkHoverMenu extends StatelessWidget { tooltipText: LocaleKeys.editor_copyLink.tr(), width: 36, height: 32, - onPressed: onCopyLink, + onPressed: widget.onCopyLink, ), FlowyIconButton( icon: FlowySvg(FlowySvgs.toolbar_link_edit_m), tooltipText: LocaleKeys.editor_editLink.tr(), width: 36, height: 32, - onPressed: onEditLink, + onPressed: widget.onEditLink, ), FlowyIconButton( icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), tooltipText: LocaleKeys.editor_removeLink.tr(), width: 36, height: 32, - onPressed: onRemoveLink, + onPressed: widget.onRemoveLink, ), ], ), @@ -339,13 +391,13 @@ class LinkHoverMenu extends StatelessWidget { ), MouseRegion( cursor: SystemMouseCursors.click, - onEnter: onEnter, - onExit: onExit, + onEnter: widget.onEnter, + onExit: widget.onExit, child: GestureDetector( - onTap: onOpenLink, + onTap: widget.onOpenLink, child: Container( - width: triggerSize.width, - height: triggerSize.height, + width: widget.triggerSize.width, + height: widget.triggerSize.height, color: Colors.black.withAlpha(1), ), ), @@ -353,6 +405,46 @@ class LinkHoverMenu extends StatelessWidget { ], ); } + + Future getPageView() async { + final viewId = href.split('/').lastOrNull ?? ''; + final (view, isInTrash, isDeleted) = + await ViewBackendService.getMentionPageStatus(viewId); + if (mounted) { + setState(() { + currentView = view; + }); + } + } + + Widget buildLinkWidget() { + final view = currentView; + if (isPage && view == null) { + return SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(), + ); + } + String text = ''; + if (isPage && view != null) { + text = view.name; + if (text.isEmpty) { + text = LocaleKeys.document_title_placeholder.tr(); + } + } else { + text = href; + } + return FlowyTooltip( + message: text, + preferBelow: false, + child: FlowyText.regular( + text, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 20, + fontSize: 14, + ), + ); + } } class HoverTriggerKey { @@ -367,7 +459,11 @@ class HoverTriggerKey { other is HoverTriggerKey && runtimeType == other.runtimeType && nodeId == other.nodeId && - selection == other.selection; + isSelectionSame(other.selection); + + bool isSelectionSame(Selection other) => + (selection.start == other.start && selection.end == other.end) || + (selection.start == other.end && selection.end == other.start); @override int get hashCode => nodeId.hashCode ^ selection.hashCode; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart index 4fcd9e3b8b..97fd6abdad 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart @@ -12,6 +12,8 @@ import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/util/link_util.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -25,12 +27,16 @@ class LinkSearchTextField { this.onEscape, this.onEnter, this.onDataRefresh, + this.initialViewId = '', + required this.currentViewId, String? initialSearchText, }) : textEditingController = TextEditingController( - text: initialSearchText ?? '', + text: isUri(initialSearchText ?? '') ? initialSearchText : '', ); final TextEditingController textEditingController; + final String initialViewId; + final String currentViewId; final ItemScrollController searchController = ItemScrollController(); late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); final List searchedViews = []; @@ -43,7 +49,7 @@ class LinkSearchTextField { String get searchText => textEditingController.text; - bool get isButtonEnable => searchText.isNotEmpty; + bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText); bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty; @@ -58,7 +64,11 @@ class LinkSearchTextField { recentViews.clear(); } - Widget buildTextField({bool autofocus = false}) { + Widget buildTextField({ + bool autofocus = false, + bool showError = false, + required BuildContext context, + }) { return TextFormField( autovalidateMode: AutovalidateMode.onUserInteraction, autofocus: autofocus, @@ -81,6 +91,8 @@ class LinkSearchTextField { }, decoration: LinkStyle.buildLinkTextFieldInputDecoration( LocaleKeys.document_toolbar_linkInputHint.tr(), + context, + showErrorBorder: showError, ), ); } @@ -177,29 +189,41 @@ class LinkSearchTextField { bool isSelected, ValueChanged? onSubmittedPageLink, ) { + final viewName = view.name; + final displayName = viewName.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : viewName; + final isCurrent = initialViewId == view.id; return SizedBox( height: 32, child: FlowyButton( isSelected: isSelected, - leftIcon: buildIcon(view), + leftIcon: buildIcon(view, padding: EdgeInsets.zero), text: FlowyText.regular( - view.name, + displayName, overflow: TextOverflow.ellipsis, fontSize: 14, figmaLineHeight: 20, ), + rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null, onTap: () => onSubmittedPageLink?.call(view), ), ); } - Widget buildIcon(ViewPB view) { + Widget buildIcon( + ViewPB view, { + EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4), + }) { if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20)); final iconData = view.icon.toEmojiIconData(); - return RawEmojiIconWidget( - emoji: iconData, - emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20, - lineHeight: 1, + return Padding( + padding: padding, + child: RawEmojiIconWidget( + emoji: iconData, + emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20, + lineHeight: 1, + ), ); } @@ -288,6 +312,7 @@ class LinkSearchTextField { final views = sectionViews .unique((e) => e.item.id) .map((e) => e.item) + .where((e) => e.id != currentViewId) .take(5) .toList(); recentViews.clear(); @@ -303,13 +328,14 @@ class LinkSearchTextField { ?.items .where( (view) => - view.name.toLowerCase().contains(search.toLowerCase()) || - (view.name.isEmpty && search.isEmpty) || - (view.name.isEmpty && - LocaleKeys.menuAppHeader_defaultNewPageName - .tr() - .toLowerCase() - .contains(search.toLowerCase())), + (view.id != currentViewId) && + (view.name.toLowerCase().contains(search.toLowerCase()) || + (view.name.isEmpty && search.isEmpty) || + (view.name.isEmpty && + LocaleKeys.menuAppHeader_defaultNewPageName + .tr() + .toLowerCase() + .contains(search.toLowerCase()))), ) .take(10) .toList(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart index ee63269dfc..f29583c9b4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart @@ -1,19 +1,33 @@ +import 'package:appflowy/util/theme_extension.dart'; import 'package:flutter/material.dart'; class LinkStyle { - static const borderColor = Color(0xFFE8ECF3); static const textTertiary = Color(0xFF99A1A8); + static const textStatusError = Color(0xffE71D32); static const fillThemeThick = Color(0xFF00B5FF); static const shadowMedium = Color(0x1F22251F); static const textPrimary = Color(0xFF1F2329); - static InputDecoration buildLinkTextFieldInputDecoration(String hintText) { - const border = OutlineInputBorder( + static Color borderColor(BuildContext context) => + Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0xffbdbdbd); + + static InputDecoration buildLinkTextFieldInputDecoration( + String hintText, + BuildContext context, { + bool showErrorBorder = false, + }) { + final border = OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderSide: BorderSide(color: LinkStyle.borderColor), + borderSide: BorderSide( + color: borderColor(context), + ), ); final enableBorder = border.copyWith( - borderSide: BorderSide(color: LinkStyle.fillThemeThick), + borderSide: BorderSide( + color: showErrorBorder + ? LinkStyle.textStatusError + : LinkStyle.fillThemeThick, + ), ); const hintStyle = TextStyle( fontSize: 14, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart deleted file mode 100644 index 8e114bf4c8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:ui'; - -import 'package:bloc/bloc.dart'; - -class ToolbarCubit extends Cubit { - ToolbarCubit(this.onDismissCallback) : super(ToolbarState._()); - - final VoidCallback onDismissCallback; - - void dismiss() { - onDismissCallback.call(); - } -} - -class ToolbarState { - const ToolbarState._(); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index d665230a59..77d94451c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; @@ -68,7 +69,7 @@ class _FormatToolbarItem extends ToolbarItem { final hoverColor = isHighlight ? highlightColor : EditorStyleCustomizer.toolbarHoverColor(context); - final isDark = Theme.of(context).brightness == Brightness.dark; + final isDark = !Theme.of(context).isLightMode; final child = FlowyIconButton( width: 36, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 2f2f75f0f0..8b32ebdde9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -78,7 +79,7 @@ class _HighlightColorPickerWidgetState } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final iconColor = AFThemeExtensionV2.of(context).icon_primary; final child = FlowyIconButton( width: 36, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index fb241d5309..693c7a64ce 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -1,15 +1,17 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'toolbar_id_enum.dart'; const kIsPageLink = 'is_page_link'; @@ -28,10 +30,10 @@ final customLinkItem = ToolbarItem( ); }); + final isDark = !Theme.of(context).isLightMode; final hoverColor = isHref ? highlightColor : EditorStyleCustomizer.toolbarHoverColor(context); - final toolbarCubit = context.read(); final child = FlowyIconButton( width: 36, @@ -41,16 +43,20 @@ final customLinkItem = ToolbarItem( icon: FlowySvg( FlowySvgs.toolbar_link_m, size: Size.square(20.0), - color: Theme.of(context).iconTheme.color, + color: (isDark && isHref) + ? Color(0xFF282E3A) + : AFThemeExtensionV2.of(context).icon_primary, ), onPressed: () { - toolbarCubit?.dismiss(); - if (isHref) { - getIt().call( - HoverTriggerKey(nodes.first.id, selection), - ); + getIt().hideToolbar(); + if (!isHref) { + final viewId = context.read()?.documentId ?? ''; + showLinkCreateMenu(context, editorState, selection, viewId); } else { - showLinkCreateMenu(context, editorState, selection); + WidgetsBinding.instance.addPostFrameCallback((_) { + getIt() + .call(HoverTriggerKey(nodes.first.id, selection)); + }); } }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 189cf64bd4..525eebe917 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_to import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'toolbar_id_enum.dart'; @@ -77,7 +78,7 @@ class _TextColorPickerWidgetState extends State { } Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; + final iconColor = AFThemeExtensionV2.of(context).icon_primary; final child = FlowyIconButton( width: 36, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart index fde59eee51..46b707a8d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart @@ -1,9 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/startup/startup.dart'; @@ -288,7 +289,7 @@ class _MoreOptionActionListState extends State { popoverController: suggestionsPopoverController, popoverDirection: PopoverDirection.leftWithTopAligned, showOffset: Offset(-8, height), - onSelect: () => context.read()?.dismiss(), + onSelect: () => getIt().hideToolbar(), child: buildCommandItem( MoreOptionCommand.suggestions, rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), @@ -308,7 +309,7 @@ class _MoreOptionActionListState extends State { popoverController: textAlignPopoverController, popoverDirection: PopoverDirection.leftWithTopAligned, showOffset: Offset(-8, 0), - onSelect: () => context.read()?.dismiss(), + onSelect: () => getIt().hideToolbar(), highlightColor: highlightColor, child: buildCommandItem( MoreOptionCommand.textAlign, @@ -379,13 +380,14 @@ enum MoreOptionCommand { (attributes) => attributes[AppFlowyRichTextKeys.href] != null, ); }); - context.read()?.dismiss(); + getIt().hideToolbar(); if (isHref) { getIt().call( HoverTriggerKey(nodes.first.id, selection), ); } else { - showLinkCreateMenu(context, editorState, selection); + final viewId = context.read()?.documentId ?? ''; + showLinkCreateMenu(context, editorState, selection, viewId); } } else if (this == strikethrough) { await editorState.toggleAttribute(name); diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index bf5dcfdafc..7a282b3856 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/util/expand_views.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; @@ -187,6 +188,9 @@ Future initGetIt( getIt.registerSingleton(PluginSandbox()); getIt.registerSingleton(ViewExpanderRegistry()); getIt.registerSingleton(LinkHoverTriggers()); + getIt.registerSingleton( + FloatingToolbarController(), + ); await DependencyResolver.resolve(getIt, mode); } From 5d33d723e9fbdcc13fb2b358a47a94d5f7760f1a Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 1 Apr 2025 09:25:49 +0800 Subject: [PATCH 259/384] fix: the checklist item will disappear when reordering (#7659) --- .../desktop_row_detail_checklist_cell.dart | 20 ++++---- .../cell_editor/checklist_cell_editor.dart | 18 ++++---- .../document/application/document_bloc.dart | 2 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++++---------- 4 files changed, 40 insertions(+), 46 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index b10d63d2d4..ab0533819a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; @@ -16,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'package:universal_platform/universal_platform.dart'; import '../editable_cell_skeleton/checklist.dart'; @@ -201,19 +200,16 @@ class _ChecklistItems extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), proxyDecorator: (child, index, _) => Material( color: Colors.transparent, - child: Stack( - children: [ - BlocProvider.value( + child: MouseRegion( + cursor: UniversalPlatform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: IgnorePointer( + child: BlocProvider.value( value: context.read(), child: child, ), - MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: const SizedBox.expand(), - ), - ], + ), ), ), buildDefaultDragHandles: false, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index b788d6bd38..9853f9c1bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -14,6 +14,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:universal_platform/universal_platform.dart'; import '../../application/cell/bloc/checklist_cell_bloc.dart'; import 'checklist_cell_textfield.dart'; @@ -125,19 +126,16 @@ class ChecklistItemList extends StatelessWidget { shrinkWrap: true, proxyDecorator: (child, index, _) => Material( color: Colors.transparent, - child: Stack( - children: [ - BlocProvider.value( + child: MouseRegion( + cursor: UniversalPlatform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: IgnorePointer( + child: BlocProvider.value( value: context.read(), child: child, ), - MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: const SizedBox.expand(), - ), - ], + ), ), ), buildDefaultDragHandles: false, diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 20703659d0..e044ffc4e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -271,7 +271,7 @@ class DocumentBloc extends Bloc { return; } - if (options.inMemoryUpdate) { + if (options.inMemoryUpdate && enableDocumentInternalLog) { Log.trace('skip transaction for in-memory update'); return; } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 From b52f681e3ceca315724de4c4fcd4463fce78c599 Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 1 Apr 2025 09:33:54 +0800 Subject: [PATCH 260/384] fix: launch review issues for non-search-bar emoji picker (#7654) * fix: some launch review issues for emoji picker * fix: revamp emoji picker which is created by character * fix: add padding for non-searchbar emoji picker --- .../lib/plugins/emoji/emoji_handler.dart | 212 +++++++++++------- .../lib/plugins/emoji/emoji_menu.dart | 114 ++++++---- .../lib/widget/flowy_tooltip.dart | 66 ++++++ 3 files changed, 265 insertions(+), 127 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart index ec86de7e8d..2109414ff4 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart @@ -2,14 +2,16 @@ import 'dart:async'; import 'dart:math'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'emoji_menu.dart'; @@ -38,27 +40,31 @@ class EmojiHandler extends StatefulWidget { } class _EmojiHandlerState extends State { - final _focusNode = FocusNode(debugLabel: 'emoji_menu_handler'); - final ItemScrollController controller = ItemScrollController(); + final focusNode = FocusNode(debugLabel: 'emoji_menu_handler'); + final scrollController = ScrollController(); late EmojiData emojiData; final List searchedEmojis = []; bool loaded = false; int invalidCounter = 0; late int startOffset; String _search = ''; + double emojiHeight = 36.0; + final configuration = EmojiPickerConfiguration( + defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, + ); set search(String search) { _search = search; _doSearch(); } - final ValueNotifier _selectedIndexNotifier = ValueNotifier(0); + final ValueNotifier selectedIndexNotifier = ValueNotifier(0); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback( - (_) => _focusNode.requestFocus(), + (_) => focusNode.requestFocus(), ); startOffset = widget.editorState.selection?.endIndex ?? 0; @@ -77,8 +83,9 @@ class _EmojiHandlerState extends State { @override void dispose() { - _focusNode.dispose(); - _selectedIndexNotifier.dispose(); + focusNode.dispose(); + selectedIndexNotifier.dispose(); + scrollController.dispose(); super.dispose(); } @@ -86,10 +93,11 @@ class _EmojiHandlerState extends State { Widget build(BuildContext context) { final noEmojis = searchedEmojis.isEmpty; return Focus( - focusNode: _focusNode, + focusNode: focusNode, onKeyEvent: onKeyEvent, child: Container( - constraints: const BoxConstraints(maxHeight: 400, maxWidth: 300), + constraints: const BoxConstraints(maxHeight: 392, maxWidth: 360), + padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: Theme.of(context).cardColor, @@ -101,52 +109,73 @@ class _EmojiHandlerState extends State { ), ], ), - child: noEmojis - ? SizedBox( - width: 400, - height: 40, - child: Center( - child: SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ), - ), - ) - : ScrollablePositionedList.builder( - itemCount: searchedEmojis.length, - itemScrollController: controller, - padding: EdgeInsets.all(8), - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemBuilder: (ctx, index) { - return ValueListenableBuilder( - valueListenable: _selectedIndexNotifier, - builder: (context, value, __) { - final selectedEmoji = searchedEmojis[index]; - final displayedEmoji = - emojiData.getEmojiById(selectedEmoji.id); - final isSelected = value == index; - return SizedBox( - height: 32, - child: FlowyButton( - text: FlowyText.medium( - '$displayedEmoji ${selectedEmoji.name}', - lineHeight: 1.0, - overflow: TextOverflow.ellipsis, - ), - isSelected: isSelected, - onTap: () => onSelect(index), - ), - ); - }, - ); - }, - ), + child: noEmojis ? buildLoading() : buildEmojis(), ), ); } - void changeSelectedIndex(int index) => _selectedIndexNotifier.value = index; + Widget buildLoading() { + return SizedBox( + width: 400, + height: 40, + child: Center( + child: SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(), + ), + ), + ); + } + + Widget buildEmojis() { + return SizedBox( + height: + (searchedEmojis.length / configuration.perLine).ceil() * emojiHeight, + child: GridView.builder( + controller: scrollController, + itemCount: searchedEmojis.length, + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: configuration.perLine, + ), + itemBuilder: (context, index) { + final currentEmoji = searchedEmojis[index]; + final emojiId = currentEmoji.id; + final emoji = emojiData.getEmojiById( + emojiId, + skinTone: configuration.defaultSkinTone, + ); + return ValueListenableBuilder( + valueListenable: selectedIndexNotifier, + builder: (context, value, child) { + final isSelected = value == index; + return SizedBox.square( + dimension: emojiHeight, + child: FlowyButton( + isSelected: isSelected, + margin: EdgeInsets.zero, + radius: Corners.s8Border, + text: ManualTooltip( + key: ValueKey('$emojiId-$isSelected'), + message: currentEmoji.name, + showAutomaticlly: isSelected, + preferBelow: false, + child: FlowyText.emoji( + emoji, + fontSize: configuration.emojiSize, + ), + ), + onTap: () => onSelect(index), + ), + ); + }, + ); + }, + ), + ); + } + + void changeSelectedIndex(int index) => selectedIndexNotifier.value = index; void loadEmojis(EmojiData data) { emojiData = data; @@ -170,6 +199,7 @@ class _EmojiHandlerState extends State { searchedEmojis.clear(); searchedEmojis.addAll(searchEmojiData.emojis.values); changeSelectedIndex(0); + _scrollToItem(); }); if (searchedEmojis.isEmpty) { widget.onDismiss.call(); @@ -177,17 +207,19 @@ class _EmojiHandlerState extends State { } KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent) { + if (event is! KeyDownEvent && event is! KeyRepeatEvent) { return KeyEventResult.ignored; } const moveKeys = [ LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, ]; if (event.logicalKey == LogicalKeyboardKey.enter) { - onSelect(_selectedIndexNotifier.value); + onSelect(selectedIndexNotifier.value); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.escape) { // Workaround to bring focus back to editor @@ -214,11 +246,7 @@ class _EmojiHandlerState extends State { return KeyEventResult.handled; } else if (event.character != null && - ![ - ...moveKeys, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - ].contains(event.logicalKey)) { + !moveKeys.contains(event.logicalKey)) { /// Prevents dismissal of context menu by notifying the parent /// that the selection change occurred from the handler. widget.onSelectionUpdate(); @@ -259,7 +287,7 @@ class _EmojiHandlerState extends State { /// focus goes back to the editor, this makes sure this handler /// receives the next keypress. /// - _focusNode.requestFocus(); + focusNode.requestFocus(); return KeyEventResult.handled; } @@ -304,41 +332,63 @@ class _EmojiHandlerState extends State { } void _moveSelection(LogicalKeyboardKey key) { - bool didChange = false; - final index = _selectedIndexNotifier.value; - if (key == LogicalKeyboardKey.arrowUp || - (key == LogicalKeyboardKey.tab && - HardwareKeyboard.instance.isShiftPressed)) { + final index = selectedIndexNotifier.value, + perLine = configuration.perLine, + remainder = index % perLine, + length = searchedEmojis.length, + currentLine = index ~/ perLine, + maxLine = (length / perLine).ceil(); + + final heightBefore = currentLine * emojiHeight; + if (key == LogicalKeyboardKey.arrowUp) { + if (currentLine == 0) { + final exceptLine = max(0, maxLine - 1); + changeSelectedIndex(min(exceptLine * perLine + remainder, length - 1)); + } else if (currentLine > 0) { + changeSelectedIndex(index - perLine); + } + } else if (key == LogicalKeyboardKey.arrowDown) { + if (currentLine == maxLine - 1) { + changeSelectedIndex(remainder); + } else if (currentLine < maxLine - 1) { + changeSelectedIndex(min(index + perLine, length - 1)); + } + } else if (key == LogicalKeyboardKey.arrowLeft) { if (index == 0) { - changeSelectedIndex(max(0, searchedEmojis.length - 1)); - didChange = true; + changeSelectedIndex(length - 1); } else if (index > 0) { changeSelectedIndex(index - 1); - didChange = true; } - } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab] - .contains(key)) { - if (index < searchedEmojis.length - 1) { - changeSelectedIndex(index + 1); - didChange = true; - } else if (index == searchedEmojis.length - 1) { + } else if (key == LogicalKeyboardKey.arrowRight) { + if (index == length - 1) { changeSelectedIndex(0); - didChange = true; + } else if (index < length - 1) { + changeSelectedIndex(index + 1); } } + final heightAfter = + (selectedIndexNotifier.value ~/ configuration.perLine) * emojiHeight; - if (mounted && didChange) { - _scrollToItem(); + if (mounted && (heightAfter != heightBefore)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToItem(); + }); } } void _scrollToItem() { final noEmojis = searchedEmojis.isEmpty; - if (noEmojis) return; - controller.scrollTo( - index: _selectedIndexNotifier.value, - duration: const Duration(milliseconds: 200), - alignment: 0.5, + if (noEmojis || !mounted) return; + final currentItem = selectedIndexNotifier.value; + final exceptHeight = (currentItem ~/ configuration.perLine) * emojiHeight; + final maxExtent = scrollController.position.maxScrollExtent; + final jumpTo = (exceptHeight - maxExtent > 10 * emojiHeight) + ? exceptHeight + : min(exceptHeight, maxExtent); + scrollController.animateTo( + jumpTo, + duration: Duration(milliseconds: 300), + curve: Curves.linear, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart index 58269a32d5..8a9ab472a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart @@ -16,14 +16,19 @@ class EmojiMenu extends EmojiMenuService { required this.editorState, this.startCharAmount = 1, this.cancelBySpaceHandler, + this.menuHeight = 400, + this.menuWidth = 300, }); final BuildContext context; final EditorState editorState; + final double menuHeight; + final double menuWidth; final bool Function()? cancelBySpaceHandler; final int startCharAmount; - + Offset _offset = Offset.zero; + Alignment _alignment = Alignment.topLeft; OverlayEntry? _menuEntry; bool selectionChangedByMenu = false; @@ -62,41 +67,11 @@ class EmojiMenu extends EmojiMenuService { return; } - const menuHeight = 400.0, menuWidth = 300.0; - const Offset menuOffset = Offset(0, 10); - final Offset editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; final Size editorSize = editorState.renderBox!.size; - final editorHeight = editorSize.height, editorWidth = editorSize.width; - // Default to opening the overlay below - Alignment alignment = Alignment.topLeft; - final firstRect = selectionRects.first; - Offset offset = firstRect.bottomRight + menuOffset; + calculateSelectionMenuOffset(selectionRects.first); - // Show above - if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) { - offset = firstRect.topRight - menuOffset; - alignment = Alignment.bottomLeft; - offset = Offset( - offset.dx, - editorHeight - offset.dy, - ); - } - - // Show on the left - if (offset.dx > (editorWidth - menuWidth)) { - alignment = alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - offset = Offset( - editorWidth - offset.dx, - offset.dy, - ); - } - - final (left, top, right, bottom) = _getPosition(alignment, offset); + final (left, top, right, bottom) = _getPosition(); _menuEntry = OverlayEntry( builder: (context) => SizedBox( @@ -175,30 +150,77 @@ class EmojiMenu extends EmojiMenuService { selectionChangedByMenu = false; } - (double? left, double? top, double? right, double? bottom) _getPosition( - Alignment alignment, - Offset offset, - ) { + (double? left, double? top, double? right, double? bottom) _getPosition() { double? left, top, right, bottom; - switch (alignment) { + switch (_alignment) { case Alignment.topLeft: - left = offset.dx; - top = offset.dy; + left = _offset.dx; + top = _offset.dy; break; case Alignment.bottomLeft: - left = offset.dx; - bottom = offset.dy; + left = _offset.dx; + bottom = _offset.dy; break; case Alignment.topRight: - right = offset.dx; - top = offset.dy; + right = _offset.dx; + top = _offset.dy; break; case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; + right = _offset.dx; + bottom = _offset.dy; break; } return (left, top, right, bottom); } + + void calculateSelectionMenuOffset(Rect rect) { + // Workaround: We can customize the padding through the [EditorStyle], + // but the coordinates of overlay are not properly converted currently. + // Just subtract the padding here as a result. + const menuOffset = Offset(0, 10); + final editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorHeight = editorState.renderBox!.size.height; + final editorWidth = editorState.renderBox!.size.width; + + // show below default + _alignment = Alignment.topLeft; + final bottomRight = rect.bottomRight; + final topRight = rect.topRight; + var offset = bottomRight + menuOffset; + _offset = Offset( + offset.dx, + offset.dy, + ); + + // show above + if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { + offset = topRight - menuOffset; + _alignment = Alignment.bottomLeft; + + _offset = Offset( + offset.dx, + editorHeight + editorOffset.dy - offset.dy, + ); + } + + // show on right + if (_offset.dx + menuWidth < editorOffset.dx + editorWidth) { + _offset = Offset( + _offset.dx, + _offset.dy, + ); + } else if (offset.dx - editorOffset.dx > menuWidth) { + // show on left + _alignment = _alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + _offset = Offset( + editorWidth - _offset.dx + editorOffset.dx, + _offset.dy, + ); + } + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index c770452fcd..8813cad09e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -50,6 +50,72 @@ class FlowyTooltip extends StatelessWidget { } } +class ManualTooltip extends StatefulWidget { + const ManualTooltip({ + super.key, + this.message, + this.richMessage, + this.preferBelow, + this.margin, + this.verticalOffset, + this.padding, + this.showAutomaticlly = false, + this.child, + }); + + final String? message; + final InlineSpan? richMessage; + final bool? preferBelow; + final EdgeInsetsGeometry? margin; + final Widget? child; + final double? verticalOffset; + final EdgeInsets? padding; + final bool showAutomaticlly; + + @override + State createState() => _ManualTooltipState(); +} + +class _ManualTooltipState extends State { + final key = GlobalKey(); + + @override + void initState() { + if (widget.showAutomaticlly) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) key.currentState?.ensureTooltipVisible(); + }); + } + super.initState(); + } + + + @override + Widget build(BuildContext context) { + return Tooltip( + key: key, + margin: widget.margin, + verticalOffset: widget.verticalOffset ?? 16.0, + triggerMode: widget.showAutomaticlly ? TooltipTriggerMode.manual : null, + padding: widget.padding ?? + const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + decoration: BoxDecoration( + color: context.tooltipBackgroundColor(), + borderRadius: BorderRadius.circular(10.0), + ), + waitDuration: _tooltipWaitDuration, + message: widget.message, + textStyle: widget.message != null ? context.tooltipTextStyle() : null, + richMessage: widget.richMessage, + preferBelow: widget.preferBelow, + child: widget.child, + ); + } +} + extension FlowyToolTipExtension on BuildContext { double tooltipFontSize() => 14.0; From edd59cf4627b678880ce69b38b99e050094b3b57 Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 1 Apr 2025 14:17:04 +0800 Subject: [PATCH 261/384] fix: link menu issues (#7661) * fix: duplicated link menu issue * fix: support toolbar animation * chore: update appflowy_editor * fix: update pubspect.lock * fix: testing error --------- Co-authored-by: Lucas.Xu --- .../document/document_toolbar_test.dart | 40 +++++++++++++++++++ .../shared/document_test_operations.dart | 8 ++-- .../document/presentation/editor_page.dart | 2 +- .../desktop_toolbar/link/link_hover_menu.dart | 9 ++--- .../desktop_toolbar/link/link_styles.dart | 6 +-- .../text_suggestions_toolbar_item.dart | 5 ++- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 8 files changed, 59 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart index aaecd0ff10..f455cd479d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart @@ -184,6 +184,45 @@ void main() { 3, ); }); + + testWidgets('toolbar will not rebuild after click item', (tester) async { + const text = 'Test rebuilding'; + await prepareForToolbar(tester, text); + Finder toolbar = find.byType(DesktopFloatingToolbar); + Element toolbarElement = toolbar.evaluate().first; + final elementHashcode = toolbarElement.hashCode; + final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m), + underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m), + italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m); + + /// tap format buttons + await tester.tapButton(boldButton); + await tester.tapButton(underlineButton); + await tester.tapButton(italicButton); + toolbar = find.byType(DesktopFloatingToolbar); + toolbarElement = toolbar.evaluate().first; + + /// check if the toolbar is not rebuilt + expect(elementHashcode, toolbarElement.hashCode); + final editorState = tester.editor.getCurrentEditorState(); + + /// check text formats + expect( + editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold), + true, + ); + expect( + editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic), + true, + ); + expect( + editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline), + true, + ); + }); }); group('document toolbar: link', () { @@ -228,6 +267,7 @@ void main() { expect(toolbar, findsNothing); /// show toolbar again + await tester.editor.tapLineOfEditorAt(0); await selectText(tester, text); await tester.tapButton(linkButton); diff --git a/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart index 491ac9432c..398a3f9657 100644 --- a/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart @@ -307,9 +307,11 @@ class EditorOperations { Future openTurnIntoMenu(Path path) async { await hoverAndClickOptionMenuButton(path); await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_optionAction_turnInto.tr(), - ), + find + .findTextInFlowyText( + LocaleKeys.document_plugins_optionAction_turnInto.tr(), + ) + .first, ); await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu)); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index ee3a852cc1..c2c18e48eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -459,7 +459,7 @@ class _AppFlowyEditorPageState extends State child: DesktopFloatingToolbar( editorState: editorState, onDismiss: onDismiss, - enableAnimation: false, + enableAnimation: !isMetricsChanged, child: child, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index b07d2949d9..0d71611516 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -221,7 +221,7 @@ class _LinkHoverTriggerState extends State { if (isPage) { final viewId = href.split('/').lastOrNull ?? ''; if (viewId.isEmpty) { - await afLaunchUrlString(href); + await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); } else { final (view, isInTrash, isDeleted) = await ViewBackendService.getMentionPageStatus(viewId); @@ -230,7 +230,7 @@ class _LinkHoverTriggerState extends State { } } } else { - await afLaunchUrlString(href); + await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); } } @@ -486,8 +486,7 @@ class LinkHoverTriggers { void call(HoverTriggerKey key) { final callbacks = _map[key] ?? {}; - for (final callback in callbacks) { - callback.call(); - } + if (callbacks.isEmpty) return; + callbacks.first.call(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart index f29583c9b4..cabc00a312 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart @@ -9,7 +9,7 @@ class LinkStyle { static const textPrimary = Color(0xFF1F2329); static Color borderColor(BuildContext context) => - Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0xffbdbdbd); + Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0x64BDBDBD); static InputDecoration buildLinkTextFieldInputDecoration( String hintText, @@ -18,9 +18,7 @@ class LinkStyle { }) { final border = OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderSide: BorderSide( - color: borderColor(context), - ), + borderSide: BorderSide(color: borderColor(context)), ); final enableBorder = border.copyWith( borderSide: BorderSide( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index c7f3b513f3..a0aded3f8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -84,12 +84,14 @@ class _SuggestionsActionListState extends State { void initState() { super.initState(); refreshSuggestions(); + editorState.selectionNotifier.addListener(refreshSuggestions); } @override void dispose() { - super.dispose(); + editorState.selectionNotifier.removeListener(refreshSuggestions); popoverController.close(); + super.dispose(); } @override @@ -290,6 +292,7 @@ class _SuggestionsActionListState extends State { } currentSuggestionItem = suggestions.where((item) => item.type == suggestionType).first; + if (mounted) setState(() {}); } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1e156a4530..5c8beaaed8 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5ad9d77" - resolved-ref: "5ad9d771a8496dea95a9c5b1ec77f76df3983037" + ref: d5c9f64 + resolved-ref: d5c9f64ebe23642f8c0e12282816992da8b8180a url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f623932a16..59871276b8 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -184,7 +184,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "5ad9d77" + ref: "d5c9f64" appflowy_editor_plugins: git: From e2ff12415c23ed57b1355c3bfabf855cc30d5e06 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 1 Apr 2025 17:35:22 +0800 Subject: [PATCH 262/384] chore: update translation (#7666) --- .../home/setting/settings_popup_menu.dart | 16 +++++++- .../widgets/float_bubble/question_bubble.dart | 39 ++++++++++++------- .../float_bubble/social_media_section.dart | 25 +++++------- .../16x/help_and_documentation.svg | 3 ++ frontend/resources/translations/el-GR.json | 4 +- frontend/resources/translations/en.json | 8 ++-- 6 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 frontend/resources/flowy_icons/16x/help_and_documentation.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index cbbda8362a..521fca4fdf 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -16,6 +16,7 @@ enum _MobileSettingsPopupMenuItem { members, trash, help, + helpAndDocumentation, } class HomePageSettingsPopupMenu extends StatelessWidget { @@ -62,10 +63,16 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_trash.tr(), ), const PopupMenuDivider(height: 0.5), + _buildItem( + value: _MobileSettingsPopupMenuItem.helpAndDocumentation, + svg: FlowySvgs.help_and_documentation_s, + text: LocaleKeys.settings_popupMenuItem_helpAndDocumentation.tr(), + ), + const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.help, svg: FlowySvgs.message_support_s, - text: LocaleKeys.settings_popupMenuItem_helpAndSupport.tr(), + text: LocaleKeys.settings_popupMenuItem_getSupport.tr(), ), ], onSelected: (_MobileSettingsPopupMenuItem value) { @@ -82,6 +89,9 @@ class HomePageSettingsPopupMenu extends StatelessWidget { case _MobileSettingsPopupMenuItem.help: _openHelpPage(context); break; + case _MobileSettingsPopupMenuItem.helpAndDocumentation: + _openHelpAndDocumentationPage(context); + break; } }, child: const Padding( @@ -123,6 +133,10 @@ class HomePageSettingsPopupMenu extends StatelessWidget { void _openSettingsPage(BuildContext context) { context.push(MobileHomeSettingPage.routeName); } + + void _openHelpAndDocumentationPage(BuildContext context) { + afLaunchUrlString('https://appflowy.com/guide'); + } } class _PopupButton extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index d666e606f6..e3117c7f86 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -86,7 +86,7 @@ class _BubbleActionListState extends State { ), buildChild: (controller) { return FlowyTooltip( - message: LocaleKeys.questionBubble_help.tr(), + message: LocaleKeys.questionBubble_getSupport.tr(), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( @@ -121,22 +121,22 @@ class _BubbleActionListState extends State { if (action is BubbleActionWrapper) { switch (action.inner) { case BubbleAction.whatsNews: - afLaunchUrlString("https://www.appflowy.io/what-is-new"); + afLaunchUrlString('https://www.appflowy.io/what-is-new'); break; - case BubbleAction.help: - afLaunchUrlString("https://discord.gg/9Q2xaN37tV"); + case BubbleAction.getSupport: + afLaunchUrlString('https://discord.gg/9Q2xaN37tV'); break; case BubbleAction.debug: _DebugToast().show(); break; case BubbleAction.shortcuts: afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/shortcuts", + 'https://docs.appflowy.io/docs/appflowy/product/shortcuts', ); break; case BubbleAction.markdown: afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/markdown", + 'https://docs.appflowy.io/docs/appflowy/product/markdown', ); break; case BubbleAction.github: @@ -144,6 +144,11 @@ class _BubbleActionListState extends State { 'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose', ); break; + case BubbleAction.helpAndDocumentation: + afLaunchUrlString( + 'https://appflowy.com/guide', + ); + break; } } @@ -155,7 +160,7 @@ class _BubbleActionListState extends State { class _DebugToast { void show() async { - String debugInfo = ""; + String debugInfo = ''; debugInfo += await _getDeviceInfo(); debugInfo += await _getDocumentPath(); await Clipboard.setData(ClipboardData(text: debugInfo)); @@ -168,20 +173,21 @@ class _DebugToast { final deviceInfo = await deviceInfoPlugin.deviceInfo; return deviceInfo.data.entries - .fold('', (prev, el) => "$prev${el.key}: ${el.value}\n"); + .fold('', (prev, el) => '$prev${el.key}: ${el.value}\n'); } Future _getDocumentPath() async { return appFlowyApplicationDataDirectory().then((directory) { final path = directory.path.toString(); - return "Document: $path\n"; + return 'Document: $path\n'; }); } } enum BubbleAction { whatsNews, - help, + helpAndDocumentation, + getSupport, debug, shortcuts, markdown, @@ -204,8 +210,10 @@ extension QuestionBubbleExtension on BubbleAction { switch (this) { case BubbleAction.whatsNews: return LocaleKeys.questionBubble_whatsNew.tr(); - case BubbleAction.help: - return LocaleKeys.questionBubble_help.tr(); + case BubbleAction.helpAndDocumentation: + return LocaleKeys.questionBubble_helpAndDocumentation.tr(); + case BubbleAction.getSupport: + return LocaleKeys.questionBubble_getSupport.tr(); case BubbleAction.debug: return LocaleKeys.questionBubble_debug_name.tr(); case BubbleAction.shortcuts: @@ -221,7 +229,12 @@ extension QuestionBubbleExtension on BubbleAction { switch (this) { case BubbleAction.whatsNews: return const FlowySvg(FlowySvgs.star_s); - case BubbleAction.help: + case BubbleAction.helpAndDocumentation: + return const FlowySvg( + FlowySvgs.help_and_documentation_s, + size: Size.square(16.0), + ); + case BubbleAction.getSupport: return const FlowySvg(FlowySvgs.message_support_s); case BubbleAction.debug: return const FlowySvg(FlowySvgs.debug_s); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart index 43b3ab9b62..8b58557455 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart @@ -25,18 +25,13 @@ class SocialMediaSection extends CustomActionCell { action: SocialMediaWrapper(social), itemHeight: ActionListSizes.itemHeight, onSelected: (action) { - switch (action.inner) { - case SocialMedia.reddit: - afLaunchUrlString( - 'https://www.reddit.com/r/AppFlowy/', - ); - case SocialMedia.twitter: - afLaunchUrlString( - 'https://x.com/appflowy', - ); - case SocialMedia.forum: - afLaunchUrlString('https://forum.appflowy.io/'); - } + final url = switch (action.inner) { + SocialMedia.reddit => 'https://www.reddit.com/r/AppFlowy/', + SocialMedia.twitter => 'https://x.com/appflowy', + SocialMedia.forum => 'https://forum.appflowy.com/', + }; + + afLaunchUrlString(url); }, ); }, @@ -85,11 +80,11 @@ extension QuestionBubbleExtension on SocialMedia { String get name { switch (this) { case SocialMedia.forum: - return "Community Forum"; + return 'Community Forum'; case SocialMedia.twitter: - return "Twitter – @appflowy"; + return 'Twitter – @appflowy'; case SocialMedia.reddit: - return "Reddit – r/appflowy"; + return 'Reddit – r/appflowy'; } } diff --git a/frontend/resources/flowy_icons/16x/help_and_documentation.svg b/frontend/resources/flowy_icons/16x/help_and_documentation.svg new file mode 100644 index 0000000000..e4c68c2583 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/help_and_documentation.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json index 34f43b87ac..6d6524a8bf 100644 --- a/frontend/resources/translations/el-GR.json +++ b/frontend/resources/translations/el-GR.json @@ -723,7 +723,7 @@ }, "url": { "launch": "Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης", - "copy": "Copy link to clipboard", + "copy": "Copied link to clipboard", "textFieldHint": "Enter a URL", "copiedNotification": "Copied to clipboard!" }, @@ -1403,4 +1403,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e0c735e2ff..f498538a1a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -307,7 +307,8 @@ "questionBubble": { "shortcuts": "Shortcuts", "whatsNew": "What's new?", - "help": "Help & Support", + "helpAndDocumentation": "Help & documentation", + "getSupport": "Get support", "markdown": "Markdown", "debug": { "name": "Debug Info", @@ -507,7 +508,8 @@ "settings": "Settings", "members": "Members", "trash": "Trash", - "helpAndSupport": "Help & Support" + "helpAndDocumentation": "Help & documentation", + "getSupport": "Get Support" }, "sites": { "title": "Sites", @@ -1645,7 +1647,7 @@ }, "url": { "launch": "Open link in browser", - "copy": "Copy link to clipboard", + "copy": "Copied link to clipboard", "textFieldHint": "Enter a URL", "copiedNotification": "Copied to clipboard!" }, From 031a88f4c45dd481edfac857771e1a58a09162ab Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 1 Apr 2025 21:26:24 +0800 Subject: [PATCH 263/384] fix: translation hotfix --- frontend/resources/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f498538a1a..27b98fdce5 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -862,7 +862,7 @@ "localAIRunning": "Local AI is running", "localAINotReadyRetryLater": "Local AI is initializing, please retry later", "localAIDisabled": "You are using local AI, but it is disabled. Please go to settings to enable it or try different model", - "localAIInitializing": "Local AI is loading. This may take a few minutes depending on your device", + "localAIInitializing": "Local AI is loading. This may take a few seconds depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI.", "restartLocalAI": "Restart", From 7f41feb959aeac076f77b271f2f05979f2dcccd0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 1 Apr 2025 21:41:20 +0800 Subject: [PATCH 264/384] chore: update changelog (#7671) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db399773b..289f7fd004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ # Release Notes +## Version 0.8.8 - 01/04/2025 +### New Features +- Added support for selecting AI models in AI writer +- Revamped link menu in toolbar +- Added support for using ":" to add emojis in documents +- Passed the history of past AI prompts and responses to AI writer +### Bug Fixes +- Improved AI writer scrolling user experience +- Fixed issue where checklist items would disappear during reordering +- Fixed numbered lists generated by AI to maintain the same index as the input + ## Version 0.8.7 - 18/03/2025 ### New Features - Made local AI free and integrated with Ollama From f8a17dac00262e69f7f6ba003df94b0529c93eaf Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Wed, 2 Apr 2025 02:50:50 +0100 Subject: [PATCH 265/384] =?UTF-8?q?chore:=20update=20translations=20with?= =?UTF-8?q?=20Fink=20=F0=9F=90=A6=20(#7669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/resources/translations/ar-SA.json | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index bf7e9f4d6a..cc5d11719d 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -256,6 +256,12 @@ "bulletWithImageDescription": "@:chat.changeFormat.bullet مع الصورة", "tableWithImageDescription": "@:chat.changeFormat.table مع الصورة" }, + "switchModel": { + "label": "تبديل النموذج", + "localModel": "النموذج المحلي", + "cloudModel": "نموذج السحابة", + "autoModel": "آلي" + }, "selectBanner": { "saveButton": "أضف إلى...", "selectMessages": "حدد الرسائل", @@ -858,6 +864,8 @@ "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", + "localAINotReadyRetryLater": "جاري تهيئة الذكاء الاصطناعي المحلي، يرجى المحاولة مرة أخرى لاحقًا", + "localAIDisabled": "أنت تستخدم الذكاء الاصطناعي المحلي، ولكنه مُعطّل. يُرجى الانتقال إلى الإعدادات لتفعيله أو تجربة نموذج آخر.", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", @@ -875,9 +883,13 @@ "activeOfflineAI": "نشط", "downloadOfflineAI": "التنزيل", "openModelDirectory": "افتح المجلد", + "laiNotReady": "لم يتم تثبيت تطبيق الذكاء الاصطناعي المحلي بشكل صحيح.", + "ollamaNotReady": "خادم Ollama غير جاهز.", "pleaseFollowThese": "اتبع هؤلاء", "instructions": "التعليمات", "installOllamaLai": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", + "modelsMissing": "لم يتم العثور على النماذج المطلوبة.", + "downloadModel": "لتنزيلها.", "startLocalAI": "قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي" } }, @@ -2133,6 +2145,7 @@ "toolbar": { "resetToDefaultFont": "إعادة تعيين إلى الافتراضي", "textSize": "حجم النص", + "textColor": "لون النص", "h1": "العنوان 1", "h2": "العنوان 2", "h3": "العنوان 3", @@ -2143,9 +2156,15 @@ "textAlign": "محاذاة النص", "moreOptions": "المزيد من الخيارات", "font": "الخط", + "inlineCode": "الكود المضمن", "suggestions": "اقتراحات", "turnInto": "تحول إلى", - "equation": "معادلة" + "equation": "معادلة", + "insert": "إدراج", + "linkInputHint": "لصق الرابط أو البحث عن الصفحات", + "pageOrURL": "الصفحة أو عنوان URL", + "linkName": "اسم الرابط", + "linkNameHint": "اسم رابط الإدخال" }, "errorBlock": { "theBlockIsNotSupported": "الإصدار الحالي لا يدعم هذا الحقل.", @@ -2255,11 +2274,11 @@ "layoutDateField": "تقويم التخطيط بواسطة", "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا" + "clickToOpen": "انقر لفتح السجل" }, "referencedCalendarPrefix": "نظرا ل", "quickJumpYear": "انتقل إلى", @@ -3187,7 +3206,8 @@ "editing": "تحرير", "analyzing": "تحليل", "continueWritingEmptyDocumentTitle": "استمر في كتابة الخطأ", - "continueWritingEmptyDocumentDescription": "نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!" + "continueWritingEmptyDocumentDescription": "نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!", + "more": "أكثر" }, "autoUpdate": { "criticalUpdateTitle": "التحديث ضروري للمتابعة", From 120f22c6fc98b2614dc02a3ac8933e3b13415769 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Apr 2025 13:08:45 +0800 Subject: [PATCH 266/384] chore: notify model change after applying local ai model --- .../lib/flutter/af_dropdown_menu.dart | 19 +++++++- .../setting_ai_view/model_selection.dart | 6 ++- .../settings/shared/settings_dropdown.dart | 3 ++ frontend/rust-lib/flowy-ai/src/ai_manager.rs | 44 +++++++++++++++++-- .../rust-lib/flowy-ai/src/event_handler.rs | 5 +-- .../flowy-ai/src/local_ai/controller.rs | 6 ++- .../flowy-ai/src/local_ai/resource.rs | 4 +- 7 files changed, 73 insertions(+), 14 deletions(-) diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index 56a61e120b..157be012b1 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -16,6 +16,8 @@ const double _kMinimumWidth = 112.0; const double _kDefaultHorizontalPadding = 12.0; +typedef CompareFunction = bool Function(T? left, T? right); + // Navigation shortcuts to move the selected menu items up or down. final Map _kMenuTraversalShortcuts = { @@ -86,6 +88,7 @@ class AFDropdownMenu extends StatefulWidget { this.requestFocusOnTap, this.expandedInsets, this.searchCallback, + this.selectOptionCompare, required this.dropdownMenuEntries, }); @@ -267,6 +270,11 @@ class AFDropdownMenu extends StatefulWidget { /// which contains the contents of the text input field. final SearchCallback? searchCallback; + /// Defines the compare function for the menu items. + /// + /// Defaults to null. If this is null, the menu items will be sorted by the label. + final CompareFunction? selectOptionCompare; + @override State> createState() => _AFDropdownMenuState(); } @@ -301,7 +309,16 @@ class _AFDropdownMenuState extends State> { filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); final int index = filteredEntries.indexWhere( - (DropdownMenuEntry entry) => entry.value == widget.initialSelection, + (DropdownMenuEntry entry) { + if (widget.selectOptionCompare != null) { + return widget.selectOptionCompare!( + entry.value, + widget.initialSelection, + ); + } else { + return entry.value == widget.initialSelection; + } + }, ); if (index != -1) { _textEditingController.value = TextEditingValue( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 30ff4addde..7357c2951c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -21,7 +21,6 @@ class AIModelSelection extends StatelessWidget { previous.availableModels != current.availableModels, builder: (context, state) { final models = state.availableModels?.models; - if (models == null) { return const SizedBox( // Using same height as SettingsDropdown to avoid layout shift @@ -31,6 +30,7 @@ class AIModelSelection extends StatelessWidget { final localModels = models.where((model) => model.isLocal).toList(); final cloudModels = models.where((model) => !model.isLocal).toList(); + final selectedModel = state.availableModels!.selectedModel; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), @@ -48,7 +48,9 @@ class AIModelSelection extends StatelessWidget { onChanged: (model) => context .read() .add(SettingsAIEvent.selectModel(model)), - selectedOption: state.availableModels!.selectedModel, + selectedOption: selectedModel, + selectOptionCompare: (left, right) => + left?.name == right?.name, options: [...localModels, ...cloudModels] .map( (model) => buildDropdownMenuEntry( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 3b2e883210..e392ed91f0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -16,9 +16,11 @@ class SettingsDropdown extends StatefulWidget { this.onChanged, this.actions, this.expandWidth = true, + this.selectOptionCompare, }); final T selectedOption; + final CompareFunction? selectOptionCompare; final List> options; final void Function(T)? onChanged; final List? actions; @@ -52,6 +54,7 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, + selectOptionCompare: widget.selectOptionCompare, textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( fontFamily: fontFamilyUsed, fontWeight: FontWeight.w400, diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 1aac5b0109..5e50f768f1 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -3,7 +3,7 @@ use crate::entities::{ AIModelPB, AvailableModelsPB, ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::local_ai::controller::LocalAIController; +use crate::local_ai::controller::{LocalAIController, LocalAISetting}; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; @@ -280,6 +280,24 @@ impl AIManager { Ok(()) } + pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { + let previous_model = self.local_ai.get_local_ai_setting().chat_model_name; + self.local_ai.update_local_ai_setting(setting).await?; + let current_model = self.local_ai.get_local_ai_setting().chat_model_name; + + if previous_model != current_model { + info!( + "[AI Plugin] update global active model, previous: {}, current: {}", + previous_model, current_model + ); + let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); + let model = AIModel::local(current_model, "".to_string()); + self.update_selected_model(source_key, model).await?; + } + + Ok(()) + } + async fn get_workspace_select_model(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let model = self @@ -358,6 +376,10 @@ impl AIManager { } pub async fn update_selected_model(&self, source: String, model: AIModel) -> FlowyResult<()> { + info!( + "[Model Selection] update {} selected model: {:?}", + source, model + ); let source_key = ai_available_models_key(&source); self .store_preferences @@ -418,9 +440,15 @@ impl AIManager { trace!("[Model Selection]: Available models: {:?}", models); + let mut current_active_local_ai_model = None; + // If user enable local ai, then add local ai model to the list. if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - models.push(AIModel::local(local_model, "".to_string())); + let model = AIModel::local(local_model, "".to_string()); + current_active_local_ai_model = Some(model.clone()); + + trace!("[Model Selection] current local ai model: {}", model.name); + models.push(model); } if models.is_empty() { @@ -454,8 +482,16 @@ impl AIManager { user_selected_model = local_ai_model.clone(); } }, - Some(model) => { - trace!("[Model Selection] user select model: {:?}", model); + Some(mut model) => { + trace!("[Model Selection] user previous select model: {:?}", model); + if model.is_local { + if let Some(local_ai_model) = ¤t_active_local_ai_model { + if local_ai_model.name != model.name { + model = local_ai_model.clone(); + } + } + } + user_selected_model = model; }, } diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 4a36a5d37a..72a271f5a1 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -369,9 +369,6 @@ pub(crate) async fn update_local_ai_setting_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager - .local_ai - .update_local_ai_setting(data.into()) - .await?; + ai_manager.update_local_ai_setting(data.into()).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index b63401b829..1a125a4b47 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -292,8 +292,10 @@ impl LocalAIController { setting, std::thread::current().id() ); - self.resource.set_llm_setting(setting).await?; - self.reload().await?; + + if let Ok(_) = self.resource.set_llm_setting(setting).await { + self.reload().await?; + } Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index c40a9a9b88..b384b30f4a 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -139,12 +139,14 @@ impl LocalAIResourceController { pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { self.resource_service.store_setting(setting)?; if let Some(resource) = self.calculate_pending_resources().await? { + let resource = LackOfAIResourcePB::from(resource); chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, ChatNotification::LocalAIResourceUpdated, ) - .payload(LackOfAIResourcePB::from(resource)) + .payload(resource.clone()) .send(); + return Err(FlowyError::local_ai().with_context(format!("{:?}", resource))); } Ok(()) } From ed5334a7d6abaf890e4648298f073f2c17b6aea7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Apr 2025 13:38:29 +0800 Subject: [PATCH 267/384] chore: clippy --- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 1a125a4b47..05f6313655 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -293,7 +293,7 @@ impl LocalAIController { std::thread::current().id() ); - if let Ok(_) = self.resource.set_llm_setting(setting).await { + if self.resource.set_llm_setting(setting).await.is_ok() { self.reload().await?; } Ok(()) From 913924d8d3de8d3a2019fe12e80d8deea21bb29d Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 2 Apr 2025 14:15:14 +0800 Subject: [PATCH 268/384] =?UTF-8?q?fix:=20emoji=20menu=20should=20only=20b?= =?UTF-8?q?e=20triggered=20when=20=E2=80=9C:=E2=80=9D=20has=20a=20followed?= =?UTF-8?q?=20letter=20(#7672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uncategorized/emoji_shortcut_test.dart | 15 ++---- .../plugins/emoji/emoji_actions_command.dart | 49 +++++++++++++---- .../lib/plugins/emoji/emoji_handler.dart | 52 +++++-------------- .../lib/plugins/emoji/emoji_menu.dart | 15 ++++-- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 6 files changed, 69 insertions(+), 68 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index f539835b9b..aba3a7be06 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -43,12 +43,12 @@ void main() { }); group('insert emoji by colon', () { - Future createNewDocumentAndShowEmojiList(WidgetTester tester) async { + Future createNewDocumentAndShowEmojiList(WidgetTester tester, {String? search}) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); await tester.createNewPageWithNameUnderParent(); await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(':'); + await tester.ime.insertText(':${search ?? 'a'}'); await tester.pumpAndSettle(Duration(seconds: 1)); } @@ -106,11 +106,10 @@ void main() { }); testWidgets('insert with searching', (tester) async { - await createNewDocumentAndShowEmojiList(tester); + await createNewDocumentAndShowEmojiList(tester, search: 's'); /// search for `smiling eyes`, IME is not working, use keyboard input final searchText = [ - LogicalKeyboardKey.keyS, LogicalKeyboardKey.keyM, LogicalKeyboardKey.keyI, LogicalKeyboardKey.keyL, @@ -138,16 +137,10 @@ void main() { }); testWidgets('start searching with sapce', (tester) async { - await createNewDocumentAndShowEmojiList(tester); + await createNewDocumentAndShowEmojiList(tester, search: ' '); /// emoji list is showing final emojiHandler = find.byType(EmojiHandler); - expect(emojiHandler, findsOneWidget); - - /// input space - await tester.simulateKeyEvent(LogicalKeyboardKey.space); - - /// emoji list is dismissed expect(emojiHandler, findsNothing); }); }); diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart index 1942a1fd98..ffe25be8bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart @@ -1,21 +1,27 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/cupertino.dart'; import 'package:universal_platform/universal_platform.dart'; import 'emoji_menu.dart'; -const emojiCharacter = ':'; +const _emojiCharacter = ':'; +final _letterRegExp = RegExp(r'^[a-zA-Z]$'); CharacterShortcutEvent emojiCommand(BuildContext context) => CharacterShortcutEvent( key: 'Opens Emoji Menu', - character: emojiCharacter, - handler: (editorState) { - emojiMenuService ??= EmojiMenu( + character: '', + regExp: _letterRegExp, + handler: (editorState) async { + return false; + }, + handlerWithCharacter: (editorState, character) { + emojiMenuService = EmojiMenu( context: context, editorState: editorState, ); - return emojiCommandHandler(editorState, context); + return emojiCommandHandler(editorState, context, character); }, ); @@ -24,6 +30,7 @@ EmojiMenuService? emojiMenuService; Future emojiCommandHandler( EditorState editorState, BuildContext context, + String character, ) async { final selection = editorState.selection; @@ -35,10 +42,30 @@ Future emojiCommandHandler( await editorState.deleteSelection(selection); } - await editorState.insertTextAtPosition( - emojiCharacter, - position: selection.start, - ); - emojiMenuService?.show(); - return true; + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || + delta == null || + delta.isEmpty || + node.type == CodeBlockKeys.type) { + return false; + } + + if (selection.end.offset > 0) { + final plain = delta.toPlainText(); + + final previousCharacter = plain[selection.end.offset - 1]; + if (previousCharacter != _emojiCharacter) return false; + if (!context.mounted) return false; + + await editorState.insertTextAtPosition( + character, + position: selection.start, + ); + + emojiMenuService?.show(character); + return true; + } + + return false; } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart index 2109414ff4..26769ba7d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; @@ -25,6 +24,7 @@ class EmojiHandler extends StatefulWidget { required this.onEmojiSelect, this.startCharAmount = 1, this.cancelBySpaceHandler, + this.initialSearchText = '', }); final EditorState editorState; @@ -33,6 +33,7 @@ class EmojiHandler extends StatefulWidget { final VoidCallback onSelectionUpdate; final SelectEmojiItemHandler onEmojiSelect; final int startCharAmount; + final String initialSearchText; final bool Function()? cancelBySpaceHandler; @override @@ -47,7 +48,7 @@ class _EmojiHandlerState extends State { bool loaded = false; int invalidCounter = 0; late int startOffset; - String _search = ''; + late String _search = widget.initialSearchText; double emojiHeight = 36.0; final configuration = EmojiPickerConfiguration( defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, @@ -67,7 +68,7 @@ class _EmojiHandlerState extends State { (_) => focusNode.requestFocus(), ); - startOffset = widget.editorState.selection?.endIndex ?? 0; + startOffset = (widget.editorState.selection?.endIndex ?? 0) - 1; if (kCachedEmojiData != null) { loadEmojis(kCachedEmojiData!); @@ -186,11 +187,14 @@ class _EmojiHandlerState extends State { loaded = true; }); } + WidgetsBinding.instance.addPostFrameCallback((_) { + _doSearch(); + }); } - Future _doSearch() async { - if (!loaded) return; - if (_search.startsWith(' ')) { + void _doSearch() { + if (!loaded || !mounted) return; + if (_search.startsWith(' ') || _search.isEmpty) { widget.onDismiss.call(); return; } @@ -266,42 +270,13 @@ class _EmojiHandlerState extends State { return KeyEventResult.handled; } - if ([LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight] - .contains(event.logicalKey)) { - widget.onSelectionUpdate(); - - event.logicalKey == LogicalKeyboardKey.arrowLeft - ? widget.editorState.moveCursorForward() - : widget.editorState.moveCursorBackward(SelectionMoveRange.character); - - /// If cursor moves before @ then dismiss menu - /// If cursor moves after @search.length then dismiss menu - final selection = widget.editorState.selection; - if (selection != null && - (selection.endIndex < startOffset || - selection.endIndex > (startOffset + _search.length))) { - widget.onDismiss.call(); - } - - /// Workaround: When using the move cursor methods, it seems the - /// focus goes back to the editor, this makes sure this handler - /// receives the next keypress. - /// - focusNode.requestFocus(); - - return KeyEventResult.handled; - } - return KeyEventResult.handled; } void onSelect(int index) { widget.onEmojiSelect.call( context, - ( - startOffset - widget.startCharAmount, - _search.length + widget.startCharAmount - ), + (startOffset - widget.startCharAmount, startOffset + _search.length), emojiData.getEmojiById(searchedEmojis[index].id), ); widget.onDismiss.call(); @@ -324,8 +299,7 @@ class _EmojiHandlerState extends State { .getTextInSelection( selection.copyWith( start: selection.start.copyWith(offset: startOffset), - end: selection.start - .copyWith(offset: startOffset + _search.length + 1), + end: selection.start.copyWith(offset: startOffset + _search.length + 1), ), ) .join(); @@ -406,7 +380,7 @@ class _EmojiHandlerState extends State { search = delta.toPlainText().substring( startOffset, - startOffset - 1 + _search.length, + startOffset + _search.length - 1, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart index 8a9ab472a6..4aff4cf6cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart @@ -5,7 +5,7 @@ import 'emoji_actions_command.dart'; import 'emoji_handler.dart'; abstract class EmojiMenuService { - void show(); + void show(String character); void dismiss(); } @@ -31,6 +31,7 @@ class EmojiMenu extends EmojiMenuService { Alignment _alignment = Alignment.topLeft; OverlayEntry? _menuEntry; bool selectionChangedByMenu = false; + String initialCharacter = ''; @override void dismiss() { @@ -56,7 +57,8 @@ class EmojiMenu extends EmojiMenuService { void _onSelectionUpdate() => selectionChangedByMenu = true; @override - void show() { + void show(String character) { + initialCharacter = character; WidgetsBinding.instance.addPostFrameCallback((_) => _show()); } @@ -97,6 +99,7 @@ class EmojiMenu extends EmojiMenuService { onSelectionUpdate: _onSelectionUpdate, startCharAmount: startCharAmount, cancelBySpaceHandler: cancelBySpaceHandler, + initialSearchText: initialCharacter, onEmojiSelect: ( BuildContext context, (int, int) replacement, @@ -109,10 +112,14 @@ class EmojiMenu extends EmojiMenuService { editorState.document.nodeAtPath(selection.end.path); if (node == null) return; final transaction = editorState.transaction - ..replaceText( + ..deleteText( + node, + replacement.$1, + replacement.$2 - replacement.$1, + ) + ..insertText( node, replacement.$1, - replacement.$2, emoji, ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5c8beaaed8..083f945636 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: d5c9f64 - resolved-ref: d5c9f64ebe23642f8c0e12282816992da8b8180a + ref: "552f95f" + resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 59871276b8..e6e377fa24 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -184,7 +184,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "d5c9f64" + ref: "552f95f" appflowy_editor_plugins: git: From d74b0bf6e14440ae8f8ad7bbe142db3f70d18f89 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 2 Apr 2025 16:09:52 +0800 Subject: [PATCH 269/384] fix: only log document event when enableDocumentInternalLog is enabled (#7676) --- .../lib/plugins/document/application/document_bloc.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index e044ffc4e5..0a54ec5753 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -271,8 +271,10 @@ class DocumentBloc extends Bloc { return; } - if (options.inMemoryUpdate && enableDocumentInternalLog) { - Log.trace('skip transaction for in-memory update'); + if (options.inMemoryUpdate) { + if (enableDocumentInternalLog) { + Log.trace('skip transaction for in-memory update'); + } return; } From 10dd0fa438d7223c40f5981f859300041904227a Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 4 Apr 2025 14:41:13 +0800 Subject: [PATCH 270/384] feat: implement new color tokens design (#7684) * fix: missing AFThemeExtensionV2 on mobile * feat: add appflowy_ui package --- .../appflowy_flutter/analysis_options.yaml | 1 + frontend/appflowy_flutter/ios/Podfile.lock | 46 +- .../appearance/mobile_appearance.dart | 7 + .../packages/appflowy_ui/.gitignore | 31 + .../packages/appflowy_ui/.metadata | 10 + .../packages/appflowy_ui/CHANGELOG.md | 3 + .../packages/appflowy_ui/LICENSE | 1 + .../packages/appflowy_ui/README.md | 39 + .../appflowy_ui/analysis_options.yaml | 29 + .../packages/appflowy_ui/example/.gitignore | 45 ++ .../packages/appflowy_ui/example/.metadata | 30 + .../packages/appflowy_ui/example/README.md | 41 + .../appflowy_ui/example/analysis_options.yaml | 28 + .../appflowy_ui/example/lib/main.dart | 109 +++ .../example/lib/src/buttons/buttons_page.dart | 287 +++++++ .../lib/src/textfield/textfield_page.dart | 77 ++ .../appflowy_ui/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + .../packages/appflowy_ui/example/pubspec.yaml | 24 + .../appflowy_ui/example/test/widget_test.dart | 30 + .../packages/appflowy_ui/lib/appflowy_ui.dart | 4 + .../component/button/base_button/base.dart | 54 ++ .../button/base_button/base_button.dart | 79 ++ .../button/base_button/base_text_button.dart | 51 ++ .../lib/src/component/button/button.dart | 16 + .../button/filled_button/filled_button.dart | 125 ++++ .../filled_icon_text_button.dart | 199 +++++ .../filled_button/filled_text_button.dart | 141 ++++ .../button/ghost_button/ghost_button.dart | 96 +++ .../ghost_button/ghost_icon_text_button.dart | 141 ++++ .../ghost_button/ghost_text_button.dart | 116 +++ .../outlined_button/outlined_button.dart | 168 +++++ .../outlined_icon_text_button.dart | 226 ++++++ .../outlined_button/outlined_text_button.dart | 204 +++++ .../lib/src/component/component.dart | 2 + .../src/component/textfield/textfield.dart | 177 +++++ .../lib/src/theme/appflowy_theme.dart | 72 ++ .../theme/border_radius/border_radius.dart | 17 + .../background/background_color_scheme.dart | 15 + .../theme/color_scheme/base/base_scheme.dart | 33 + .../lib/src/theme/color_scheme/base/blue.dart | 27 + .../src/theme/color_scheme/base/green.dart | 24 + .../src/theme/color_scheme/base/magenta.dart | 25 + .../src/theme/color_scheme/base/neutral.dart | 49 ++ .../src/theme/color_scheme/base/orange.dart | 25 + .../src/theme/color_scheme/base/purple.dart | 25 + .../lib/src/theme/color_scheme/base/red.dart | 27 + .../src/theme/color_scheme/base/subtle.dart | 265 +++++++ .../src/theme/color_scheme/base/yellow.dart | 24 + .../src/theme/color_scheme/border/border.dart | 49 ++ .../border/border_color_scheme.dart | 49 ++ .../brand/brand_color_scheme.dart | 25 + .../src/theme/color_scheme/color_scheme.dart | 8 + .../color_scheme/fill/fill_color_scheme.dart | 93 +++ .../color_scheme/icon/icon_color_theme.dart | 21 + .../surface/surface_color_scheme.dart | 11 + .../color_scheme/text/text_color_scheme.dart | 47 ++ .../lib/src/theme/data/builder.dart | 324 ++++++++ .../appflowy_ui/lib/src/theme/data/data.dart | 238 ++++++ .../appflowy_ui/lib/src/theme/dimensions.dart | 17 + .../lib/src/theme/spacing/spacing.dart | 17 + .../text_style/base/default_text_style.dart | 298 ++++++++ .../lib/src/theme/text_style/text_style.dart | 17 + .../appflowy_ui/lib/src/theme/theme.dart | 7 + .../packages/appflowy_ui/pubspec.yaml | 17 + .../flowy_icons/20x/anonymous_mode.svg | 3 + 91 files changed, 5852 insertions(+), 23 deletions(-) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/.gitignore create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/.metadata create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/LICENSE create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/README.md create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/README.md create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml create mode 100644 frontend/resources/flowy_icons/20x/anonymous_mode.svg diff --git a/frontend/appflowy_flutter/analysis_options.yaml b/frontend/appflowy_flutter/analysis_options.yaml index 8da401ef26..4579b2d8c5 100644 --- a/frontend/appflowy_flutter/analysis_options.yaml +++ b/frontend/appflowy_flutter/analysis_options.yaml @@ -4,6 +4,7 @@ analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" + - "packages/**/*.dart" linter: rules: diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 92e52a1a79..4b7ed5d639 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 - appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 + appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 - flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 - keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 + saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index eda3153459..7778f1c9ce 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { @@ -29,6 +30,7 @@ class MobileAppearance extends BaseAppearance { ); final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); + final isLight = brightness == Brightness.light; final theme = brightness == Brightness.light ? appTheme.lightTheme @@ -283,6 +285,11 @@ class MobileAppearance extends BaseAppearance { toolbarHoverColor: theme.toolbarHoverColor, ), ToolbarColorExtension.fromBrightness(brightness), + isLight + ? lightAFThemeV2 + : darkAFThemeV2.copyWith( + icon_primary: theme.icon, + ), ], ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore new file mode 100644 index 0000000000..da0bb7ce97 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/.metadata new file mode 100644 index 0000000000..79932b61d5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" + channel: "[user-branch]" + +project_type: package diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md b/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE b/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/README.md new file mode 100644 index 0000000000..75218b1842 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/README.md @@ -0,0 +1,39 @@ +# AppFlowy UI + +AppFlowy UI is a Flutter package that provides a collection of reusable UI components following the AppFlowy design system. These components are designed to be consistent, accessible, and easy to use. + +## Features + +- **Design System Components**: Buttons, text fields, and more UI components that follow the AppFlowy design system +- **Theming**: Consistent theming across all components with light and dark mode support + +## Installation + +Add the following to your *app's* `pubspec.yaml` file: + +```yaml +dependencies: + appflowy_ui: ^1.0.0 +``` + +## Supported components + +- [x] Button +- [x] TextField +- [ ] Avatar +- [ ] Checkbox +- [ ] Grid +- [ ] Link +- [ ] Loading & Progress Indicator +- [ ] Menu +- [ ] Message Box +- [ ] Navigation Bar +- [ ] Popover +- [ ] Scroll Bar +- [ ] Tab Bar +- [ ] Toggle +- [ ] Tooltip + +## Reference + +Figma: https://www.figma.com/design/aphWa2OgkqyIragpatdk7a/Design-System diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml new file mode 100644 index 0000000000..abba19b4fe --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml @@ -0,0 +1,29 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - require_trailing_commas + + - prefer_collection_literals + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + + - sized_box_for_whitespace + - use_decorated_box + + - unnecessary_parenthesis + - unnecessary_await_in_return + - unnecessary_raw_strings + + - avoid_unnecessary_containers + - avoid_redundant_argument_values + - avoid_unused_constructor_parameters + + - always_declare_return_types + + - sort_constructors_first + - unawaited_futures + +errors: + invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore new file mode 100644 index 0000000000..79c113f9b5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata new file mode 100644 index 0000000000..777c932a64 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: macos + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md new file mode 100644 index 0000000000..2ccc9e658d --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md @@ -0,0 +1,41 @@ +# AppFlowy UI Example + +This example demonstrates how to use the `appflowy_ui` package in a Flutter application. + +## Getting Started + +To run this example: + +1. Ensure you have Flutter installed and set up on your machine +2. Clone this repository +3. Navigate to the example directory: + ```bash + cd example + ``` +4. Get the dependencies: + ```bash + flutter pub get + ``` +5. Run the example: + ```bash + flutter run + ``` + +## Features Demonstrated + +- Basic app structure using AppFlowy UI components +- Material 3 design integration +- Responsive layout + +## Project Structure + +- `lib/main.dart`: The main application file +- `pubspec.yaml`: Project dependencies and configuration + +## Additional Resources + +For more information about the AppFlowy UI package, please refer to: + +- The main package documentation +- [AppFlowy Website](https://appflowy.io) +- [AppFlowy GitHub Repository](https://github.com/AppFlowy-IO/AppFlowy) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml new file mode 100644 index 0000000000..0d2902135c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart new file mode 100644 index 0000000000..067e42858b --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -0,0 +1,109 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_ui_example/src/buttons/buttons_page.dart'; +import 'package:appflowy_ui_example/src/textfield/textfield_page.dart'; +import 'package:flutter/material.dart'; + +enum ThemeMode { + light, + dark, +} + +final themeMode = ValueNotifier(ThemeMode.light); + +void main() { + runApp( + const MyApp(), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: themeMode, + builder: (context, themeMode, child) { + return AppFlowyTheme( + data: themeMode == ThemeMode.light + ? AppFlowyThemeData.light() + : AppFlowyThemeData.dark(), + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'AppFlowy UI Example', + theme: themeMode == ThemeMode.light + ? ThemeData.light() + : ThemeData.dark(), + home: const MyHomePage( + title: 'AppFlowy UI', + ), + ), + ); + }, + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + required this.title, + }); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final tabs = [ + Tab(text: 'Button'), + Tab(text: 'TextField'), + ]; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text( + widget.title, + style: theme.textStyle.title.enhanced( + color: theme.textColorScheme.primary, + ), + ), + actions: [ + IconButton( + icon: Icon( + theme.brightness == Brightness.light + ? Icons.dark_mode + : Icons.light_mode, + ), + onPressed: _toggleTheme, + tooltip: 'Toggle theme', + ), + ], + ), + body: TabBarView( + children: [ + ButtonsPage(), + TextFieldPage(), + ], + ), + bottomNavigationBar: TabBar( + tabs: tabs, + ), + floatingActionButton: null, + ), + ); + } + + void _toggleTheme() { + themeMode.value = + themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart new file mode 100644 index 0000000000..0d0c018222 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart @@ -0,0 +1,287 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class ButtonsPage extends StatelessWidget { + const ButtonsPage({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSection( + 'Filled Text Buttons', + [ + AFFilledTextButton.primary( + text: 'Primary Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFFilledTextButton.destructive( + text: 'Destructive Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFFilledTextButton.disabled( + text: 'Disabled Button', + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Filled Icon Text Buttons', + [ + AFFilledButton.primary( + onTap: () {}, + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.add, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + const SizedBox(width: 8), + Text( + 'Primary Button', + style: TextStyle( + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + AFFilledButton.destructive( + onTap: () {}, + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.delete, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + const SizedBox(width: 8), + Text( + 'Destructive Button', + style: TextStyle( + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + AFFilledButton.disabled( + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.block, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + 'Disabled Button', + style: TextStyle( + color: + AppFlowyTheme.of(context).textColorScheme.tertiary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Outlined Text Buttons', + [ + AFOutlinedTextButton.normal( + text: 'Normal Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFOutlinedTextButton.destructive( + text: 'Destructive Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFOutlinedTextButton.disabled( + text: 'Disabled Button', + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Outlined Icon Text Buttons', + [ + AFOutlinedButton.normal( + onTap: () {}, + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.add, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Normal Button', + style: TextStyle( + color: + AppFlowyTheme.of(context).textColorScheme.primary, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + AFOutlinedButton.destructive( + onTap: () {}, + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.delete, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.error, + ), + const SizedBox(width: 8), + Text( + 'Destructive Button', + style: TextStyle( + color: AppFlowyTheme.of(context).textColorScheme.error, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + AFOutlinedButton.disabled( + builder: (context, isHovering, disabled) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.block, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + 'Disabled Button', + style: TextStyle( + color: + AppFlowyTheme.of(context).textColorScheme.tertiary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Ghost Buttons', + [ + AFGhostTextButton.primary( + text: 'Primary Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFGhostTextButton.disabled( + text: 'Disabled Button', + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Button with alignment', + [ + SizedBox( + width: 200, + child: AFFilledTextButton.primary( + text: 'Left Button', + onTap: () {}, + alignment: Alignment.centerLeft, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 200, + child: AFFilledTextButton.primary( + text: 'Center Button', + onTap: () {}, + alignment: Alignment.center, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 200, + child: AFFilledTextButton.primary( + text: 'Right Button', + onTap: () {}, + alignment: Alignment.centerRight, + ), + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'Button Sizes', + [ + AFFilledTextButton.primary( + text: 'Small Button', + onTap: () {}, + size: AFButtonSize.s, + ), + const SizedBox(width: 16), + AFFilledTextButton.primary( + text: 'Medium Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFFilledTextButton.primary( + text: 'Large Button', + onTap: () {}, + size: AFButtonSize.l, + ), + const SizedBox(width: 16), + AFFilledTextButton.primary( + text: 'Extra Large Button', + onTap: () {}, + size: AFButtonSize.xl, + ), + ], + ), + ], + ), + ); + } + + Widget _buildSection(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart new file mode 100644 index 0000000000..280e43818c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart @@ -0,0 +1,77 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class TextFieldPage extends StatelessWidget { + const TextFieldPage({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSection( + 'TextField with hint text', + [ + AFTextField( + hintText: 'Please enter your name', + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'TextField with initial text', + [ + AFTextField( + initialText: 'https://appflowy.com', + ), + ], + ), + const SizedBox(height: 32), + _buildSection( + 'TextField with validator ', + [ + AFTextField( + validator: (controller) { + if (controller.text.isEmpty) { + return (true, 'This field is required'); + } + + final emailRegex = + RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(controller.text)) { + return (true, 'Please enter a valid email address'); + } + + return (false, ''); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildSection(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore new file mode 100644 index 0000000000..746adbb6b9 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..345181d730 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "appflowy_ui_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..04d5b736e6 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000..b3c1761412 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..a2ec33f19f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 zcmeEugo5nb1G~3xi~y`}h6XHx5j$(L*3|5S2UfkG$|UCNI>}4f?MfqZ+HW-sRW5RKHEm z^unW*Xx{AH_X3Xdvb%C(Bh6POqg==@d9j=5*}oEny_IS;M3==J`P0R!eD6s~N<36C z*%-OGYqd0AdWClO!Z!}Y1@@RkfeiQ$Ib_ z&fk%T;K9h`{`cX3Hu#?({4WgtmkR!u3ICS~|NqH^fdNz>51-9)OF{|bRLy*RBv#&1 z3Oi_gk=Y5;>`KbHf~w!`u}!&O%ou*Jzf|Sf?J&*f*K8cftMOKswn6|nb1*|!;qSrlw= zr-@X;zGRKs&T$y8ENnFU@_Z~puu(4~Ir)>rbYp{zxcF*!EPS6{(&J}qYpWeqrPWW< zfaApz%<-=KqxrqLLFeV3w0-a0rEaz9&vv^0ZfU%gt9xJ8?=byvNSb%3hF^X_n7`(fMA;C&~( zM$cQvQ|g9X)1AqFvbp^B{JEX$o;4iPi?+v(!wYrN{L}l%e#5y{j+1NMiT-8=2VrCP zmFX9=IZyAYA5c2!QO96Ea-6;v6*$#ZKM-`%JCJtrA3d~6h{u+5oaTaGE)q2b+HvdZ zvHlY&9H&QJ5|uG@wDt1h99>DdHy5hsx)bN`&G@BpxAHh$17yWDyw_jQhhjSqZ=e_k z_|r3=_|`q~uA47y;hv=6-o6z~)gO}ZM9AqDJsR$KCHKH;QIULT)(d;oKTSPDJ}Jx~G#w-(^r<{GcBC*~4bNjfwHBumoPbU}M)O za6Hc2ik)2w37Yyg!YiMq<>Aov?F2l}wTe+>h^YXcK=aesey^i)QC_p~S zp%-lS5%)I29WfywP(r4@UZ@XmTkqo51zV$|U|~Lcap##PBJ}w2b4*kt7x6`agP34^ z5fzu_8rrH+)2u*CPcr6I`gL^cI`R2WUkLDE5*PX)eJU@H3HL$~o_y8oMRoQ0WF9w| z6^HZDKKRDG2g;r8Z4bn+iJNFV(CG;K-j2>aj229gl_C6n12Jh$$h!}KVhn>*f>KcH z;^8s3t(ccVZ5<{>ZJK@Z`hn_jL{bP8Yn(XkwfRm?GlEHy=T($8Z1Mq**IM`zxN9>-yXTjfB18m_$E^JEaYn>pj`V?n#Xu;Z}#$- zw0Vw;T*&9TK$tKI7nBk9NkHzL++dZ^;<|F6KBYh2+XP-b;u`Wy{~79b%IBZa3h*3^ zF&BKfQ@Ej{7ku_#W#mNJEYYp=)bRMUXhLy2+SPMfGn;oBsiG_6KNL8{p1DjuB$UZB zA)a~BkL)7?LJXlCc}bB~j9>4s7tlnRHC5|wnycQPF_jLl!Avs2C3^lWOlHH&v`nGd zf&U!fn!JcZWha`Pl-B3XEe;(ks^`=Z5R zWyQR0u|do2`K3ec=YmWGt5Bwbu|uBW;6D8}J3{Uep7_>L6b4%(d=V4m#(I=gkn4HT zYni3cnn>@F@Wr<hFAY3Y~dW+3bte;70;G?kTn4Aw5nZ^s5|47 z4$rCHCW%9qa4)4vE%^QPMGf!ET!^LutY$G zqdT(ub5T5b+wi+OrV}z3msoy<4)`IPdHsHJggmog0K*pFYMhH!oZcgc5a)WmL?;TPSrerTVPp<#s+imF3v#!FuBNNa`#6 z!GdTCF|IIpz#(eV^mrYKThA4Bnv&vQet@%v9kuRu3EHx1-2-it@E`%9#u`)HRN#M? z7aJ{wzKczn#w^`OZ>Jb898^Xxq)0zd{3Tu7+{-sge-rQ z&0PME&wIo6W&@F|%Z8@@N3)@a_ntJ#+g{pUP7i?~3FirqU`rdf8joMG^ld?(9b7Iv z>TJgBg#)(FcW)h!_if#cWBh}f+V08GKyg|$P#KTS&%=!+0a%}O${0$i)kn9@G!}En zv)_>s?glPiLbbx)xk(lD-QbY(OP3;MSXM5E*P&_`Zks2@46n|-h$Y2L7B)iH{GAAq19h5-y0q>d^oy^y+soJu9lXxAe%jcm?=pDLFEG2kla40e!5a}mpe zdL=WlZ=@U6{>g%5a+y-lx)01V-x;wh%F{=qy#XFEAqcd+m}_!lQ)-9iiOL%&G??t| z?&NSdaLqdPdbQs%y0?uIIHY7rw1EDxtQ=DU!i{)Dkn~c$LG5{rAUYM1j5*G@oVn9~ zizz{XH(nbw%f|wI=4rw^6mNIahQpB)OQy10^}ACdLPFc2@ldVi|v@1nWLND?)53O5|fg`RZW&XpF&s3@c-R?aad!$WoH6u0B|}zt)L($E^@U- zO#^fxu9}Zw7Xl~nG1FVM6DZSR0*t!4IyUeTrnp@?)Z)*!fhd3)&s(O+3D^#m#bAem zpf#*aiG_0S^ofpm@9O7j`VfLU0+{$x!u^}3!zp=XST0N@DZTp!7LEVJgqB1g{psNr za0uVmh3_9qah14@M_pi~vAZ#jc*&aSm$hCNDsuQ-zPe&*Ii#2=2gP+DP4=DY z_Y0lUsyE6yaV9)K)!oI6+*4|spx2at*30CAx~6-5kfJzQ`fN8$!lz%hz^J6GY?mVH zbYR^JZ(Pmj6@vy-&!`$5soyy-NqB^8cCT40&R@|6s@m+ZxPs=Bu77-+Os7+bsz4nA3DrJ8#{f98ZMaj-+BD;M+Jk?pgFcZIb}m9N z{ct9T)Kye&2>l^39O4Q2@b%sY?u#&O9PO4@t0c$NUXG}(DZJ<;_oe2~e==3Z1+`Zo zFrS3ns-c}ZognVBHbg#e+1JhC(Yq7==rSJQ8J~}%94(O#_-zJKwnBXihl#hUd9B_>+T& z7eHHPRC?5ONaUiCF7w|{J`bCWS7Q&xw-Sa={j-f)n5+I=9s;E#fBQB$`DDh<^mGiF zu-m_k+)dkBvBO(VMe2O4r^sf3;sk9K!xgXJU>|t9Vm8Ty;fl5pZzw z9j|}ZD}6}t;20^qrS?YVPuPRS<39d^y0#O1o_1P{tN0?OX!lc-ICcHI@2#$cY}_CY zev|xdFcRTQ_H)1fJ7S0*SpPs8e{d+9lR~IZ^~dKx!oxz?=Dp!fD`H=LH{EeC8C&z-zK$e=!5z8NL=4zx2{hl<5z*hEmO=b-7(k5H`bA~5gT30Sjy`@-_C zKM}^so9Ti1B;DovHByJkTK87cfbF16sk-G>`Q4-txyMkyQS$d}??|Aytz^;0GxvOs zPgH>h>K+`!HABVT{sYgzy3CF5ftv6hI-NRfgu613d|d1cg^jh+SK7WHWaDX~hlIJ3 z>%WxKT0|Db1N-a4r1oPKtF--^YbP=8Nw5CNt_ZnR{N(PXI>Cm$eqi@_IRmJ9#)~ZHK_UQ8mi}w^`+4$OihUGVz!kW^qxnCFo)-RIDbA&k-Y=+*xYv5y4^VQ9S)4W5Pe?_RjAX6lS6Nz#!Hry=+PKx2|o_H_3M`}Dq{Bl_PbP(qel~P@=m}VGW*pK96 zI@fVag{DZHi}>3}<(Hv<7cVfWiaVLWr@WWxk5}GDEbB<+Aj;(c>;p1qmyAIj+R!`@#jf$ zy4`q23L-72Zs4j?W+9lQD;CYIULt%;O3jPWg2a%Zs!5OW>5h1y{Qof!p&QxNt5=T( zd5fy&7=hyq;J8%86YBOdc$BbIFxJx>dUyTh`L z-oKa=OhRK9UPVRWS`o2x53bAv+py)o)kNL6 z9W1Dlk-g6Ht@-Z^#6%`9S9`909^EMj?9R^4IxssCY-hYzei^TLq7Cj>z$AJyaU5=z zl!xiWvz0U8kY$etrcp8mL;sYqGZD!Hs-U2N{A|^oEKA482v1T%cs%G@X9M?%lX)p$ zZoC7iYTPe8yxY0Jne|s)fCRe1mU=Vb1J_&WcIyP|x4$;VSVNC`M+e#oOA`#h>pyU6 z?7FeVpk`Hsu`~T3i<_4<5fu?RkhM;@LjKo6nX>pa%8dSdgPO9~Jze;5r>Tb1Xqh5q z&SEdTXevV@PT~!O6z|oypTk7Qq+BNF5IQ(8s18c=^0@sc8Gi|3e>VKCsaZ?6=rrck zl@oF5Bd0zH?@15PxSJIRroK4Wa?1o;An;p0#%ZJ^tI=(>AJ2OY0GP$E_3(+Zz4$AQ zW)QWl<4toIJ5TeF&gNXs>_rl}glkeG#GYbHHOv-G!%dJNoIKxn)FK$5&2Zv*AFic! z@2?sY&I*PSfZ8bU#c9fdIJQa_cQijnj39-+hS@+~e*5W3bj%A}%p9N@>*tCGOk+cF zlcSzI6j%Q|2e>QG3A<86w?cx6sBtLNWF6_YR?~C)IC6_10SNoZUHrCpp6f^*+*b8` zlx4ToZZuI0XW1W)24)92S)y0QZa);^NRTX6@gh8@P?^=#2dV9s4)Q@K+gnc{6|C}& zDLHr7nDOLrsH)L@Zy{C_2UrYdZ4V{|{c8&dRG;wY`u>w%$*p>PO_}3`Y21pk?8Wtq zGwIXTulf7AO2FkPyyh2TZXM1DJv>hI`}x`OzQI*MBc#=}jaua&czSkI2!s^rOci|V zFkp*Vbiz5vWa9HPFXMi=BV&n3?1?%8#1jq?p^3wAL`jgcF)7F4l<(H^!i=l-(OTDE zxf2p71^WRIExLf?ig0FRO$h~aA23s#L zuZPLkm>mDwBeIu*C7@n@_$oSDmdWY7*wI%aL73t~`Yu7YwE-hxAATmOi0dmB9|D5a zLsR7OQcA0`vN9m0L|5?qZ|jU+cx3_-K2!K$zDbJ$UinQy<9nd5ImWW5n^&=Gg>Gsh zY0u?m1e^c~Ug39M{{5q2L~ROq#c{eG8Oy#5h_q=#AJj2Yops|1C^nv0D1=fBOdfAG z%>=vl*+_w`&M7{qE#$xJJp_t>bSh7Mpc(RAvli9kk3{KgG5K@a-Ue{IbU{`umXrR3ra5Y7xiX42+Q%N&-0#`ae_ z#$Y6Wa++OPEDw@96Zz##PFo9sADepQe|hUy!Zzc2C(L`k9&=a8XFr+!hIS>D2{pdGP1SzwyaGLiH3j--P>U#TWw90t8{8Bt%m7Upspl#=*hS zhy|(XL6HOqBW}Og^tLX7 z+`b^L{O&oqjwbxDDTg2B;Yh2(fW>%S5Pg8^u1p*EFb z`(fbUM0`afawYt%VBfD&b3MNJ39~Ldc@SAuzsMiN%E}5{uUUBc7hc1IUE~t-Y9h@e7PC|sv$xGx=hZiMXNJxz5V(np%6u{n24iWX#!8t#>Ob$in<>dw96H)oGdTHnU zSM+BPss*5)Wz@+FkooMxxXZP1{2Nz7a6BB~-A_(c&OiM)UUNoa@J8FGxtr$)`9;|O z(Q?lq1Q+!E`}d?KemgC!{nB1JJ!B>6J@XGQp9NeQvtbM2n7F%v|IS=XWPVZY(>oq$ zf=}8O_x`KOxZoGnp=y24x}k6?gl_0dTF!M!T`={`Ii{GnT1jrG9gPh)R=RZG8lIR| z{ZJ6`x8n|y+lZuy${fuEDTAf`OP!tGySLXD}ATJO5UoZv|Xo3%7O~L63+kw}v)Ci=&tWx3bQJfL@5O18CbPlkR^IcKA zy1=^Vl-K-QBP?9^R`@;czcUw;Enbbyk@vJQB>BZ4?;DM%BUf^eZE+sOy>a){qCY6Y znYy;KGpch-zf=5|p#SoAV+ie8M5(Xg-{FoLx-wZC9IutT!(9rJ8}=!$!h%!J+vE2e z(sURwqCC35v?1>C1L)swfA^sr16{yj7-zbT6Rf26-JoEt%U?+|rQ zeBuGohE?@*!zR9)1P|3>KmJSgK*fOt>N>j}LJB`>o(G#Dduvx7@DY7};W7K;Yj|8O zGF<+gTuoIKe7Rf+LQG3-V1L^|E;F*}bQ-{kuHq}| ze_NwA7~US19sAZ)@a`g*zkl*ykv2v3tPrb4Og2#?k6Lc7@1I~+ew48N&03hW^1Cx+ zfk5Lr4-n=#HYg<7ka5i>2A@ZeJ60gl)IDX!!p zzfXZQ?GrT>JEKl7$SH!otzK6=0dIlqN)c23YLB&Krf9v-{@V8p+-e2`ujFR!^M%*; ze_7(Jh$QgoqwB!HbX=S+^wqO15O_TQ0-qX8f-|&SOuo3ZE{{9Jw5{}>MhY}|GBhO& zv48s_B=9aYQfa;d>~1Z$y^oUUaDer>7ve5+Gf?rIG4GZ!hRKERlRNgg_C{W_!3tsI2TWbX8f~MY)1Q`6Wj&JJ~*;ay_0@e zzx+mE-pu8{cEcVfBqsnm=jFU?H}xj@%CAx#NO>3 z_re3Rq%d1Y7VkKy{=S73&p;4^Praw6Y59VCP6M?!Kt7{v#DG#tz?E)`K95gH_mEvb z%$<~_mQ$ad?~&T=O0i0?`YSp?E3Dj?V>n+uTRHAXn`l!pH9Mr}^D1d@mkf+;(tV45 zH_yfs^kOGLXlN*0GU;O&{=awxd?&`{JPRr$z<1HcAO2K`K}92$wC}ky&>;L?#!(`w z68avZGvb728!vgw>;8Z8I@mLtI`?^u6R>sK4E7%=y)jpmE$fH!Dj*~(dy~-2A5Cm{ zl{1AZw`jaDmfvaB?jvKwz!GC}@-Dz|bFm1OaPw(ia#?>vF7Y5oh{NVbyD~cHB1KFn z9C@f~X*Wk3>sQH9#D~rLPslAd26@AzMh=_NkH_yTNXx6-AdbAb z{Ul89YPHslD?xAGzOlQ*aMYUl6#efCT~WI zOvyiewT=~l1W(_2cEd(8rDywOwjM-7P9!8GCL-1<9KXXO=6%!9=W++*l1L~gRSxLVd8K=A7&t52ql=J&BMQu{fa6y zXO_e>d?4X)xp2V8e3xIQGbq@+vo#&n>-_WreTTW0Yr?|YRPP43cDYACMQ(3t6(?_k zfgDOAU^-pew_f5U#WxRXB30wcfDS3;k~t@b@w^GG&<5n$Ku?tT(%bQH(@UHQGN)N|nfC~7?(etU`}XB)$>KY;s=bYGY#kD%i9fz= z2nN9l?UPMKYwn9bX*^xX8Y@%LNPFU>s#Ea1DaP%bSioqRWi9JS28suTdJycYQ+tW7 zrQ@@=13`HS*dVKaVgcem-45+buD{B;mUbY$YYULhxK)T{S?EB<8^YTP$}DA{(&)@S zS#<8S96y9K2!lG^VW-+CkfXJIH;Vo6wh)N}!08bM$I7KEW{F6tqEQ?H@(U zAqfi%KCe}2NUXALo;UN&k$rU0BLNC$24T_mcNY(a@lxR`kqNQ0z%8m>`&1ro40HX} z{{3YQ;2F9JnVTvDY<4)x+88i@MtXE6TBd7POk&QfKU-F&*C`isS(T_Q@}K)=zW#K@ zbXpcAkTT-T5k}Wj$dMZl7=GvlcCMt}U`#Oon1QdPq%>9J$rKTY8#OmlnNWBYwafhx zqFnym@okL#Xw>4SeRFejBnZzY$jbO)e^&&sHBgMP%Ygfi!9_3hp17=AwLBNFTimf0 zw6BHNXw19Jg_Ud6`5n#gMpqe%9!QB^_7wAYv8nrW94A{*t8XZu0UT&`ZHfkd(F{Px zD&NbRJP#RX<=+sEeGs2`9_*J2OlECpR;4uJie-d__m*(aaGE}HIo+3P{my@;a~9Y$ zHBXVJ83#&@o6{M+pE9^lI<4meLLFN_3rwgR4IRyp)~OF0n+#ORrcJ2_On9-78bWbG zuCO0esc*n1X3@p1?lN{qWS?l7J$^jbpeel{w~51*0CM+q9@9X=>%MF(ce~om(}?td zjkUmdUR@LOn-~6LX#=@a%rvj&>DFEoQscOvvC@&ZB5jVZ-;XzAshwx$;Qf@U41W=q zOSSjQGQV8Qi3*4DngNMIM&Cxm7z*-K`~Bl(TcEUxjQ1c=?)?wF8W1g;bAR%sM#LK( z_Op?=P%)Z+J!>vpN`By0$?B~Out%P}kCriDq@}In&fa_ZyKV+nLM0E?hfxuu%ciUz z>yAk}OydbWNl7{)#112j&qmw;*Uj&B;>|;Qwfc?5wIYIHH}s6Mve@5c5r+y)jK9i( z_}@uC(98g)==AGkVN?4>o@w=7x9qhW^ zB(b5%%4cHSV?3M?k&^py)j*LK16T^Ef4tb05-h-tyrjt$5!oo4spEfXFK7r_Gfv7#x$bsR7T zs;dqxzUg9v&GjsQGKTP*=B(;)be2aN+6>IUz+Hhw-n>^|`^xu*xvjGPaDoFh2W4-n z@Wji{5Y$m>@Vt7TE_QVQN4*vcfWv5VY-dT0SV=l=8LAEq1go*f zkjukaDV=3kMAX6GAf0QOQHwP^{Z^=#Lc)sh`QB)Ftl&31jABvq?8!3bt7#8vxB z53M{4{GR4Hl~;W3r}PgXSNOt477cO62Yj(HcK&30zsmWpvAplCtpp&mC{`2Ue*Bwu zF&UX1;w%`Bs1u%RtGPFl=&sHu@Q1nT`z={;5^c^^S~^?2-?<|F9RT*KQmfgF!7=wD@hytxbD;=9L6PZrK*1<4HMObNWehA62DtTy)q5H|57 z9dePuC!1;0MMRRl!S@VJ8qG=v^~aEU+}2Qx``h1LII!y{crP2ky*R;Cb;g|r<#ryo zju#s4dE?5CTIZKc*O4^3qWflsQ(voX>(*_JP7>Q&$%zCAIBTtKC^JUi@&l6u&t0hXMXjz_y!;r@?k|OU9aD%938^TZ>V? zqJmom_6dz4DBb4Cgs_Ef@}F%+cRCR%UMa9pi<-KHN;t#O@cA%(LO1Rb=h?5jiTs93 zPLR78p+3t>z4|j=<>2i4b`ketv}9Ax#B0)hn7@bFl;rDfP8p7u9XcEb!5*PLKB(s7wQC2kzI^@ae)|DhNDmSy1bOLid%iIap@24A(q2XI!z_hkl-$1T10 z+KKugG4-}@u8(P^S3PW4x>an;XWEF-R^gB{`t8EiP{ZtAzoZ!JRuMRS__-Gg#Qa3{<;l__CgsF+nfmFNi}p z>rV!Y6B@cC>1up)KvaEQiAvQF!D>GCb+WZsGHjDeWFz?WVAHP65aIA8u6j6H35XNYlyy8>;cWe3ekr};b;$9)0G`zsc9LNsQ&D?hvuHRpBxH)r-1t9|Stc*u<}Ol&2N+wPMom}d15_TA=Aprp zjN-X3*Af$7cDWMWp##kOH|t;c2Pa9Ml4-)o~+7P;&q8teF-l}(Jt zTGKOQqJTeT!L4d}Qw~O0aanA$Vn9Rocp-MO4l*HK)t%hcp@3k0%&_*wwpKD6ThM)R z8k}&7?)YS1ZYKMiy?mn>VXiuzX7$Ixf7EW8+C4K^)m&eLYl%#T=MC;YPvD&w#$MMf zQ=>`@rh&&r!@X&v%ZlLF42L_c=5dSU^uymKVB>5O?AouR3vGv@ei%Z|GX5v1GK2R* zi!!}?+-8>J$JH^fPu@)E6(}9$d&9-j51T^n-e0Ze%Q^)lxuex$IL^XJ&K2oi`wG}QVGk2a7vC4X?+o^z zsCK*7`EUfSuQA*K@Plsi;)2GrayQOG9OYF82Hc@6aNN5ulqs1Of-(iZQdBI^U5of^ zZg2g=Xtad7$hfYu6l~KDQ}EU;oIj(3nO#u9PDz=eO3(iax7OCmgT2p_7&^3q zg7aQ;Vpng*)kb6=sd5?%j5Dm|HczSChMo8HHq_L8R;BR5<~DVyU$8*Tk5}g0eW5x7 z%d)JFZ{(Y<#OTKLBA1fwLM*fH7Q~7Sc2Ne;mVWqt-*o<;| z^1@vo_KTYaMnO$7fbLL+qh#R$9bvnpJ$RAqG+z8h|} z3F5iwG*(sCn9Qbyg@t0&G}3fE0jGq3J!JmG2K&$urx^$z95) z7h?;4vE4W=v)uZ*Eg3M^6f~|0&T)2D;f+L_?M*21-I1pnK(pT$5l#QNlT`SidYw~o z{`)G)Asv#cue)Ax1RNWiRUQ(tQ(bzd-f2U4xlJK+)ZWBxdq#fp=A>+Qc%-tl(c)`t z$e2Ng;Rjvnbu7((;v4LF9Y1?0el9hi!g>G{^37{ z`^s-03Z5jlnD%#Mix19zkU_OS|86^_x4<0(*YbPN}mi-$L?Z4K(M|2&VV*n*ZYN_UqI?eKZi3!b)i z%n3dzUPMc-dc|q}TzvPy!VqsEWCZL(-eURDRG4+;Eu!LugSSI4Fq$Ji$Dp08`pfP_C5Yx~`YKcywlMG;$F z)R5!kVml_Wv6MSpeXjG#g?kJ0t_MEgbXlUN3k|JJ%N>|2xn8yN>>4qxh!?dGI}s|Y zDTKd^JCrRSN+%w%D_uf=Tj6wIV$c*g8D96jb^Kc#>5Fe-XxKC@!pIJw0^zu;`_yeb zhUEm-G*C=F+jW%cP(**b61fTmPn2WllBr4SWNdKe*P8VabZsh0-R|?DO=0x`4_QY) zR7sthW^*BofW7{Sak&S1JdiG?e=SfL24Y#w_)xrBVhGB-13q$>mFU|wd9Xqe-o3{6 zSn@@1@&^)M$rxb>UmFuC+pkio#T;mSnroMVZJ%nZ!uImi?%KsIX#@JU2VY(`kGb1A z7+1MEG)wd@)m^R|a2rXeviv$!emwcY(O|M*xV!9%tBzarBOG<4%gI9SW;Um_gth4=gznYzOFd)y8e+3APCkL)i-OI`;@7-mCJgE`js(M} z;~ZcW{{FMVVO)W>VZ}ILouF#lWGb%Couu}TI4kubUUclW@jEn6B_^v!Ym*(T*4HF9 zWhNKi8%sS~viSdBtnrq!-Dc5(G^XmR>DFx8jhWvR%*8!m*b*R8e1+`7{%FACAK`7 zzdy8TmBh?FVZ0vtw6npnWwM~XjF2fNvV#ZlGG z?FxHkXHN>JqrBYoPo$)zNC7|XrQfcqmEXWud~{j?La6@kbHG@W{xsa~l1=%eLly8B z4gCIH05&Y;6O2uFSopNqP|<$ml$N40^ikxw0`o<~ywS1(qKqQN!@?Ykl|bE4M?P+e zo$^Vs_+x)iuw?^>>`$&lOQOUkZ5>+OLnRA)FqgpDjW&q*WAe(_mAT6IKS9;iZBl8M z<@=Y%zcQUaSBdrs27bVK`c$)h6A1GYPS$y(FLRD5Yl8E3j0KyH08#8qLrsc_qlws; znMV%Zq8k+&T2kf%6ZO^2=AE9>?a587g%-={X}IS~P*I(NeCF9_9&`)|ok0iiIun zo+^odT0&Z4k;rn7I1v87=z!zKU(%gfB$(1mrRYeO$sbqM22Kq68z9wgdg8HBxp>_< zn9o%`f?sVO=IN#5jSX&CGODWlZfQ9A)njK2O{JutYwRZ?n0G_p&*uwpE`Md$iQxrd zoQfF^b8Ou)+3BO_3_K5y*~?<(BF@1l+@?Z6;^;U>qlB)cdro;rxOS1M{Az$s^9o5sXDCg8yD<=(pKI*0e zLk>@lo#&s0)^*Q+G)g}C0IErqfa9VbL*Qe=OT@&+N8m|GJF7jd83vY#SsuEv2s{Q> z>IpoubNs>D_5?|kXGAPgF@mb_9<%hjU;S0C8idI)a=F#lPLuQJ^7OnjJlH_Sks9JD zMl1td%YsWq3YWhc;E$H1<0P$YbSTqs`JKY%(}svsifz|h8BHguL82dBl+z0^YvWk8 zGy;7Z0v5_FJ2A$P0wIr)lD?cPR%cz>kde!=W%Ta^ih+Dh4UKdf7ip?rBz@%y2&>`6 zM#q{JXvW9ZlaSk1oD!n}kSmcDa2v6T^Y-dy+#fW^y>eS8_%<7tWXUp8U@s$^{JFfKMjDAvR z$YmVB;n3ofl!ro9RNT!TpQpcycXCR}$9k5>IPWDXEenQ58os?_weccrT+Bh5sLoiH zZ_7~%t(vT)ZTEO= zb0}@KaD{&IyK_sd8b$`Qz3%UA`nSo zn``!BdCeN!#^G;lK@G2ron*0jQhbdw)%m$2;}le@z~PSLnU-z@tL)^(p%P>OO^*Ff zNRR9oQ`W+x^+EU+3BpluwK77|B3=8QyT|$V;02bn_LF&3LhLA<#}{{)jE)}CiW%VEU~9)SW+=F%7U-iYlQ&q!#N zwI2{(h|Pi&<8_fqvT*}FLN^0CxN}#|3I9G_xmVg$gbn2ZdhbmGk7Q5Q2Tm*ox8NMo zv`iaZW|ZEOMyQga5fts?&T-eCCC9pS0mj7v0SDkD=*^MxurP@89v&Z#3q{FM!a_nr zb?KzMv`BBFOew>4!ft@A&(v-kWXny-j#egKef|#!+3>26Qq0 zv!~8ev4G`7Qk>V1TaMT-&ziqoY3IJp8_S*%^1j73D|=9&;tDZH^!LYFMmME4*Wj(S zRt~Q{aLb_O;wi4u&=}OYuj}Lw*j$@z*3>4&W{)O-oi@9NqdoU!=U%d|se&h?^$Ip# z)BY+(1+cwJz!yy4%l(aLC;T!~Ci>yAtXJb~b*yr&v7f{YCU8P|N1v~H`xmGsG)g)y z4%mv=cPd`s7a*#OR7f0lpD$ueP>w8qXj0J&*7xX+U!uat5QNk>zwU$0acn5p=$88L=jn_QCSYkTV;1~(yUem#0gB`FeqY98sf=>^@ z_MCdvylv~WL%y_%y_FE1)j;{Szj1+K7Lr_y=V+U zk6Tr;>XEqlEom~QGL!a+wOf(@ZWoxE<$^qHYl*H1a~kk^BLPn785%nQb$o;Cuz0h& za9LMx^bKEbPS%e8NM33Jr|1T|ELC(iE!FUci38xW_Y7kdHid#2ie+XZhP;2!Z;ZAM zB_cXKm)VrPK!SK|PY00Phwrpd+x0_Aa;}cDQvWKrwnQrqz##_gvHX2ja?#_{f#;bz`i>C^^ zTLDy;6@HZ~XQi7rph!mz9k!m;KchA)uMd`RK4WLK7)5Rl48m#l>b(#`WPsl<0j z-sFkSF6>Nk|LKnHtZ`W_NnxZP62&w)S(aBmmjMDKzF%G;3Y?FUbo?>b5;0j8Lhtc4 zr*8d5Y9>g@FFZaViw7c16VsHcy0u7M%6>cG1=s=Dtx?xMJSKIu9b6GU8$uSzf43Y3 zYq|U+IWfH;SM~*N1v`KJo!|yfLxTFS?oHsr3qvzeVndVV^%BWmW6re_S!2;g<|Oao z+N`m#*i!)R%i1~NO-xo{qpwL0ZrL7hli;S z3L0lQ_z}z`fdK39Mg~Zd*%mBdD;&5EXa~@H(!###L`ycr7gW`f)KRuqyHL3|uyy3h zSS^td#E&Knc$?dXs*{EnPYOp^-vjAc-h4z#XkbG&REC7;0>z^^Z}i8MxGKerEY z>l?(wReOlXEsNE5!DO&ZWyxY)gG#FSZs%fXuzA~XIAPVp-%yb2XLSV{1nH6{)5opg z(dZKckn}Q4Li-e=eUDs1Psg~5zdn1>ql(*(nn6)iD*OcVkwmKL(A{fix(JhcVB&}V zVt*Xb!{gzvV}dc446>(D=SzfCu7KB`oMjv6kPzSv&B>>HLSJP|wN`H;>oRw*tl#N) z*zZ-xwM7D*AIsBfgqOjY1Mp9aq$kRa^dZU_xw~KxP;|q(m+@e+YSn~`wEJzM|Ippb zzb@%;hB7iH4op9SqmX?j!KP2chsb79(mFossBO-Zj8~L}9L%R%Bw<`^X>hjkCY5SG z7lY!8I2mB#z)1o;*3U$G)3o0A&{0}#B;(zPd2`OF`Gt~8;0Re8nIseU z_yzlf$l+*-wT~_-cYk$^wTJ@~7i@u(CZs9FVkJCru<*yK8&>g+t*!JqCN6RH%8S-P zxH8+Cy#W?!;r?cLMC(^BtAt#xPNnwboI*xWw#T|IW^@3|q&QYY6Ehxoh@^URylR|T zne-Y6ugE^7p5bkRDWIh)?JH5V^ub82l-LuVjDr7UT^g`q4dB&mBFRWGL_C?hoeL(% zo}ocH5t7|1Mda}T!^{Qt9vmA2ep4)dQSZO>?Eq8}qRp&ZJ?-`Tnw+MG(eDswP(L*X3ahC2Ad0_wD^ff9hfzb%Jd`IXx5 zae@NMzBXJDwJS?7_%!TB^E$N8pvhOHDK$7YiOelTY`6KX8hK6YyT$tk*adwN>s^Kp zwM3wGVPhwKU*Yq-*BCs}l`l#Tej(NQ>jg*S0TN%D+GcF<14Ms6J`*yMY;W<-mMN&-K>((+P}+t+#0KPGrzjP zJ~)=Bcz%-K!L5ozIWqO(LM)l_9lVOc4*S65&DKM#TqsiWNG{(EZQw!bc>qLW`=>p-gVJ;T~aN2D_- z{>SZC=_F+%hNmH6ub%Ykih0&YWB!%sd%W5 zHC2%QMP~xJgt4>%bU>%6&uaDtSD?;Usm}ari0^fcMhi_)JZgb1g5j zFl4`FQ*%ROfYI}e7RIq^&^a>jZF23{WB`T>+VIxj%~A-|m=J7Va9FxXV^%UwccSZd zuWINc-g|d6G5;95*%{e;9S(=%yngpfy+7ao|M7S|Jb0-4+^_q-uIqVS&ufU880UDH*>(c)#lt2j zzvIEN>>$Y(PeALC-D?5JfH_j+O-KWGR)TKunsRYKLgk7eu4C{iF^hqSz-bx5^{z0h ze2+u>Iq0J4?)jIo)}V!!m)%)B;a;UfoJ>VRQ*22+ncpe9f4L``?v9PH&;5j{WF?S_C>Lq>nkChZB zjF8(*v0c(lU^ZI-)_uGZnnVRosrO4`YinzI-RSS-YwjYh3M`ch#(QMNw*)~Et7Qpy z{d<3$4FUAKILq9cCZpjvKG#yD%-juhMj>7xIO&;c>_7qJ%Ae8Z^m)g!taK#YOW3B0 zKKSMOd?~G4h}lrZbtPk)n*iOC1~mDhASGZ@N{G|dF|Q^@1ljhe=>;wusA&NvY*w%~ zl+R6B^1yZiF)YN>0ms%}qz-^U-HVyiN3R9k1q4)XgDj#qY4CE0)52%evvrrOc898^ z*^)XFR?W%g0@?|6Mxo1ZBp%(XNv_RD-<#b^?-Fs+NL^EUW=iV|+Vy*F%;rBz~pN7%-698U-VMfGEVnmEz7fL1p)-5sLT zL;Iz>FCLM$p$c}g^tbkGK1G$IALq1Gd|We@&TtW!?4C7x4l*=4oF&&sr0Hu`x<5!m zhX&&Iyjr?AkNXU_5P_b^Q3U9sy#f6ZF@2C96$>1k*E-E%DjwvA{VL0PdU~suN~DZo zm{T!>sRdp`Ldpp9olrH@(J$QyGq!?#o1bUo=XP2OEuT3`XzI>s^0P{manUaE4pI%! zclQq;lbT;nx7v3tR9U)G39h?ryrxzd0xq4KX7nO?piJZbzT_CU&O=T(Vt;>jm?MgC z2vUL#*`UcMsx%w#vvjdamHhmN!(y-hr~byCA-*iCD};#l+bq;gkwQ0oN=AyOf@8ow>Pj<*A~2*dyjK}eYdN);%!t1 z6Y=|cuEv-|5BhA?n2Db@4s%y~(%Wse4&JXw=HiO48%c6LB~Z0SL1(k^9y?ax%oj~l zf7(`iAYLdPRq*ztFC z7VtAb@s{as%&Y;&WnyYl+6Wm$ru*u!MKIg_@01od-iQft0rMjIj8e7P9eKvFnx_X5 zd%pDg-|8<>T2Jdqw>AII+fe?CgP+fL(m0&U??QL8YzSjV{SFi^vW~;wN@or_(q<0Y zRt~L}#JRcHOvm$CB)T1;;7U>m%)QYBLTR)KTARw%zoDxgssu5#v{UEVIa<>{8dtkm zXgbCGp$tfue+}#SD-PgiNT{Zu^YA9;4BnM(wZ9-biRo_7pN}=aaimjYgC=;9@g%6< zxol5sT_$<8{LiJ6{l1+sV)Z_QdbsfEAEMw!5*zz6)Yop?T0DMtR_~wfta)E6_G@k# zZRP11D}$ir<`IQ`<(kGfAS?O-DzCyuzBq6dxGTNNTK?r^?zT30mLY!kQ=o~Hv*k^w zvq!LBjW=zzIi%UF@?!g9vt1CqdwV(-2LYy2=E@Z?B}JDyVkluHtzGsWuI1W5svX~K z&?UJ45$R7g>&}SFnLnmw09R2tUgmr_w6mM9C}8GvQX>nL&5R#xBqnp~Se(I>R42`T zqZe9p6G(VzNB3QD><8+y%{e%6)sZDRXTR|MI zM#eZmao-~_`N|>Yf;a;7yvd_auTG#B?Vz5D1AHx=zpVUFe7*hME z+>KH5h1In8hsVhrstc>y0Q!FHR)hzgl+*Q&5hU9BVJlNGRkXiS&06eOBV^dz3;4d5 zeYX%$62dNOprZV$px~#h1RH?_E%oD6y;J;pF%~y8M)8pQ0olYKj6 zE+hd|7oY3ot=j9ZZ))^CCPADL6Jw%)F@A{*coMApcA$7fZ{T@3;WOQ352F~q6`Mgi z$RI6$8)a`Aaxy<8Bc;{wlDA%*%(msBh*xy$L-cBJvQ8hj#FCyT^%+Phw1~PaqyDou^JR0rxDkSrmAdjeYDFDZ`E z)G3>XtpaSPDlydd$RGHg;#4|4{aP5c_Om z2u5xgnhnA)K%8iU==}AxPxZCYC)lyOlj9as#`5hZ=<6<&DB%i_XCnt5=pjh?iusH$ z>)E`@HNZcAG&RW3Ys@`Ci{;8PNzE-ZsPw$~Wa!cP$ye+X6;9ceE}ah+3VY7Mx}#0x zbqYa}eO*FceiY2jNS&2cH9Y}(;U<^^cWC5Ob&)dZedvZA9HewU3R;gRQ)}hUdf+~Q zS_^4ds*W1T#bxS?%RH&<739q*n<6o|mV;*|1s>ly-Biu<2*{!!0#{_234&9byvn0* z5=>{95Zfb{(?h_Jk#ocR$FZ78O*UTOxld~0UF!kyGM|nH%B*qf)Jy}N!uT9NGeM19 z-@=&Y0yGGo_dw!FD>juk%P$6$qJkj}TwLBoefi;N-$9LAeV|)|-ET&culW9Sb_pc_ zp{cXI0>I0Jm_i$nSvGnYeLSSj{ccVS2wyL&0x~&5v;3Itc82 z5lIAkfn~wcY-bQB$G!ufWt%qO;P%&2B_R5UKwYxMemIaFm)qF1rA zc>gEihb=jBtsXCi0T%J37s&kt*3$s7|6)L(%UiY)6axuk{6RWIS8^+u;)6!R?Sgap z9|6<0bx~AgVi|*;zL@2x>Pbt2Bz*uv4x-`{F)XatTs`S>unZ#P^ZiyjpfL_q2z^fqgR-fbOcG=Y$q>ozkw1T6dH8-)&ww+z?E0 zR|rV(9bi6zpX3Ub>PrPK!{X>e$C66qCXAeFm)Y+lX8n2Olt7PNs*1^si)j!QmFV#t z0P2fyf$N^!dyTot&`Ew5{i5u<8D`8U`qs(KqaWq5iOF3x2!-z65-|HsyYz(MAKZ?< zCpQR;E)wn%s|&q(LVm0Ab>gdmCFJeKwVTnv@Js%!At;I=A>h=l=p^&<4;Boc{$@h< z38v`3&2wJtka@M}GS%9!+SpJ}sdtoYzMevVbnH+d_eMxN@~~ zZq@k)7V5f8u!yAX2qF3qjS7g%n$JuGrMhQF!&S^7(%Y{rP*w2FWj(v_J{+Hg*}wdWOd~pHQ19&n3RWeljK9W%sz&Y3Tm3 zR`>6YR54%qBHGa)2xbs`9cs_EsNHxsfraEgZ)?vrtooeA0sPKJK7an){ngtV@{SBa zkO6ORr1_Xqp+`a0e}sC*_y(|RKS13ikmHp3C^XkE@&wjbGWrt^INg^9lDz#B;bHiW zkK4{|cg08b!yHFSgPca5)vF&gqCgeu+c82%&FeM^Bb}GUxLy-zo)}N;#U?sJ2?G2BNe*9u_7kE5JeY!it=f`A_4gV3} z`M!HXZy#gN-wS!HvHRqpCHUmjiM;rVvpkC!voImG%OFVN3k(QG@X%e``VJSJ@Z7tb z*Onlf>z^D+&$0!4`IE$;2-NSO9HQWd+UFW(r;4hh;(j^p4H-~6OE!HQp^96v?{9Zt z;@!ZcccV%C2s6FMP#qvo4kG6C04A>XILt>JW}%0oE&HM5f6 zYLD!;My>CW+j<~=Wzev{aYtx2ZNw|ptTFV(4;9`6Tmbz6K1)fv4qPXa2mtoPt&c?P zhmO+*o8uP3ykL6E$il00@TDf6tOW7fmo?Oz_6GU^+5J=c22bWyuH#aNj!tT-^IHrJ zu{aqTYw@q;&$xDE*_kl50Jb*dp`(-^p={z}`rqECTi~3 z>0~A7L6X)=L5p#~$V}gxazgGT7$3`?a)zen>?TvAuQ+KAIAJ-s_v}O6@`h9n-sZk> z`3{IJeb2qu9w=P*@q>iC`5wea`KxCxrx{>(4{5P+!cPg|pn~;n@DiZ0Y>;k5mnKeS z!LIfT4{Lgd=MeysR5YiQKCeNhUQ;Os1kAymg6R!u?j%LF z4orCszIq_n52ulpes{(QN|zirdtBsc{9^Z72Ycb2ht?G^opkT_#|4$wa9`)8k3ilU z%ntAi`nakS1r10;#k^{-ZGOD&Z2|k=p40hRh5D7(&JG#Cty|ECOvwsSHkkSa)36$4 z?;v#%@D(=Raw(HP5s>#4Bm?f~n1@ebH}2tv#7-0l-i^H#H{PC|F@xeNS+Yw{F-&wH z07)bj8MaE6`|6NoqKM~`4%X> zKFl&7g1$Z3HB>lxn$J`P`6GSb6CE6_^NA1V%=*`5O!zP$a7Vq)IwJAki~XBLf=4TF zPYSL}>4nOGZ`fyHChq)jy-f{PKFp6$plHB2=;|>%Z^%)ecVue(*mf>EH_uO^+_zm? zJATFa9SF~tFwR#&0xO{LLf~@}s_xvCPU8TwIJgBs%FFzjm`u?1699RTui;O$rrR{# z1^MqMl5&6)G%@_k*$U5Kxq84!AdtbZ!@8FslBML}<`(Jr zenXrC6bFJP=R^FMBg7P?Pww-!a%G@kJH_zezKvuWU0>m1uyy}#Vf<$>u?Vzo3}@O% z1JR`B?~Tx2)Oa|{DQ_)y9=oY%haj!80GNHw3~qazgU-{|q+Bl~H94J!a%8UR?XsZ@ z0*ZyQugyru`V9b(0OrJOKISfi89bSVR zQy<+i_1XY}4>|D%X_`IKZUPz6=TDb)t1mC9eg(Z=tv zq@|r37AQM6A%H%GaH3szv1L^ku~H%5_V*fv$UvHl*yN4iaqWa69T2G8J2f3kxc7UE zOia@p0YNu_q-IbT%RwOi*|V|&)e5B-u>4=&n@`|WzH}BK4?33IPpXJg%`b=dr_`hU z8JibW_3&#uIN_#D&hX<)x(__jUT&lIH$!txEC@cXv$7yB&Rgu){M`9a`*PH} zRcU)pMWI2O?x;?hzR{WdzKt^;_pVGJAKKd)F$h;q=Vw$MP1XSd<;Mu;EU5ffyKIg+ z&n-Nb?h-ERN7(fix`htopPIba?0Gd^y(4EHvfF_KU<4RpN0PgVxt%7Yo99X*Pe|zR z?ytK&5qaZ$0KSS$3ZNS$$k}y(2(rCl=cuYZg{9L?KVgs~{?5adxS))Upm?LDo||`H zV)$`FF3icFmxcQshXX*1k*w3O+NjBR-AuE70=UYM*7>t|I-oix=bzDwp2*RoIwBp@r&vZukG; zyi-2zdyWJ3+E?{%?>e2Ivk`fAn&Ho(KhGSVE4C-zxM-!j01b~mTr>J|5={PrZHOgO zw@ND3=z(J7D>&C7aw{zT>GHhL2BmUX0GLt^=31RRPSnjoUO9LYzh_yegyPoAKhAQE z>#~O27dR4&LdQiak6={9_{LN}Z>;kyVYKH^d^*!`JVSXJlx#&r4>VnP$zb{XoTb=> zZsLvh>keP3fkLTIDdpf-@(ADfq4=@X=&n>dyU0%dwD{zsjCWc;r`-e~X$Q3NTz_TJ zOXG|LMQQIjGXY3o5tBm9>k6y<6XNO<=9H@IXF;63rzsC=-VuS*$E{|L_i;lZmHOD< zY92;>4spdeRn4L6pY4oUKZG<~+8U-q7ZvNOtW0i*6Q?H`9#U3M*k#4J;ek(MwF02x zUo1wgq9o6XG#W^mxl>pAD)Ll-V5BNsdVQ&+QS0+K+?H-gIBJ-ccB1=M_hxB6qcf`C zJ?!q!J4`kLhAMry4&a_0}up{CFevcjBl|N(uDM^N5#@&-nQt2>z*U}eJGi}m5f}l|IRVj-Q;a>wcLpK5RRWJ> zysdd$)Nv0tS?b~bw1=gvz3L_ZAIdDDPj)y|bp1;LE`!av!rODs-tlc}J#?erTgXRX z$@ph%*~_wr^bQYHM7<7=Q=45v|Hk7T=mDpW@OwRy3A_v`ou@JX5h!VI*e((v*5Aq3 zVYfB4<&^Dq5%^?~)NcojqK`(VXP$`#w+&VhQOn%;4pCkz;NEH6-FPHTQ+7I&JE1+Ozq-g43AEZV>ceQ^9PCx zZG@OlEF~!Lq@5dttlr%+gNjRyMwJdJU(6W_KpuVnd{3Yle(-p#6erIRc${l&qx$HA z89&sp=rT7MJ=DuTL1<5{)wtUfpPA|Gr6Q2T*=%2RFm@jyo@`@^*{5{lFPgv>84|pv z%y{|cVNz&`9C*cUely>-PRL)lHVErAKPO!NQ3<&l5(>Vp(MuJnrOf^4qpIa!o3D7( z1bjn#Vv$#or|s7Hct5D@%;@48mM%ISY7>7@ft8f?q~{s)@BqGiupoK1BAg?PyaDQ1 z`YT8{0Vz{zBwJ={I4)#ny{RP{K1dqzAaQN_aaFC%Z>OZ|^VhhautjDavGtsQwx@WH zr|1UKk^+X~S*RjCY_HN!=Jx>b6J8`Q(l4y|mc<6jnkHVng^Wk(A13-;AhawATsmmE#H%|8h}f1frs2x@Fwa_|ea+$tdG2Pz{7 z!ox^w^>^Cv4e{Xo7EQ7bxCe8U+LZG<_e$RnR?p3t?s^1Mb!ieB z#@45r*PTc_yjh#P=O8Zogo+>1#|a2nJvhOjIqKK1U&6P)O%5s~M;99O<|Y9zomWTL z666lK^QW`)cXV_^Y05yQZH3IRCW%25BHAM$c0>w`x!jh^15Zp6xYb!LoQ zr+RukTw0X2mxN%K0%=8|JHiaA3pg5+GMfze%9o5^#upx0M?G9$+P^DTx7~qq9$Qoi zV$o)yy zuUq>3c{_q+HA5OhdN*@*RkxRuD>Bi{Ttv_hyaaB;XhB%mJ2Cb{yL;{Zu@l{N?!GKE7es6_9J{9 zO(tmc0ra2;@oC%SS-8|D=omQ$-Dj>S)Utkthh{ovD3I%k}HoranSepC_yco2Q8 zY{tAuPIhD{X`KbhQIr%!t+GeH%L%q&p z3P%<-S0YY2Emjc~Gb?!su85}h_qdu5XN2XJUM}X1k^!GbwuUPT(b$Ez#LkG6KEWQB z7R&IF4srHe$g2R-SB;inW9T{@+W+~wi7VQd?}7||zi!&V^~o0kM^aby7YE_-B63^d zf_uo8#&C77HBautt_YH%v6!Q>H?}(0@4pv>cM6_7dHJ)5JdyV0Phi!)vz}dv{*n;t zf(+#Hdr=f8DbJqbMez)(n>@QT+amJ7g&w6vZ-vG^H1v~aZqG~u!1D(O+jVAG0EQ*aIsr*bsBdbD`)i^FNJ z&B@yxqPFCRGT#}@dmu-{0vp47xk(`xNM6E=7QZ5{tg6}#zFrd8Pb_bFg7XP{FsYP8 zbvWqG6#jfg*4gvY9!gJxJ3l2UjP}+#QMB(*(?Y&Q4PO`EknE&Cb~Yb@lCbk;-KY)n zzbjS~W5KZ3FV%y>S#$9Sqi$FIBCw`GfPDP|G=|y32VV-g@a1D&@%_oAbB@cAUx#aZ zlAPTJ{iz#Qda8(aNZE&0q+8r3&z_Ln)b=5a%U|OEcc3h1f&8?{b8ErEbilrun}mh3 z$1o^$-XzIiH|iGoJA`w`o|?w3m*NX|sd$`Mt+f*!hyJvQ2fS*&!SYn^On-M|pHGlu z4SC5bM7f6BAkUhGuN*w`97LLkbCx=p@K5RL2p>YpDtf{WTD|d3ucb6iVZ-*DRtoEA zCC5(x)&e=giR_id>5bE^l%Mxx>0@FskpCD4oq@%-Fg$8IcdRwkfn;DsjoX(v;mt3d z_4Mnf#Ft4x!bY!7Hz?RRMq9;5FzugD(sbt4up~6j?-or+ch~y_PqrM2hhTToJjR_~ z)E1idgt7EW>G*9%Q^K;o_#uFjX!V2pwfpgi>}J&p_^QlZki!@#dkvR`p?bckC`J*g z=%3PkFT3HAX2Q+dShHUbb1?ZcK8U7oaufLTCB#1W{=~k0Jabgv>q|H+GU=f-y|{p4 zwN|AE+YbCgx=7vlXE?@gkXW9PaqbO#GB=4$o0FkNT#EI?aLVd2(qnPK$Yh%YD%v(mdwn}bgsxyIBI^)tY?&G zi^2JfClZ@4b{xFjyTY?D61w@*ez2@5rWLpG#34id?>>oPg{`4F-l`7Lg@D@Hc}On} zx%BO4MsLYosLGACJ-d?ifZ35r^t*}wde>AAWO*J-X%jvD+gL9`u`r=kP zyeJ%FqqKfz8e_3K(M1RmB?gIYi{W7Z<THP2ihue0mbpu5n(x_l|e1tw(q!#m5lmef6ktqIb${ zV+ee#XRU}_dDDUiV@opHZ@EbQ<9qIZJMDsZDkW0^t3#j`S)G#>N^ZBs8k+FJhAfu< z%u!$%dyP3*_+jUvCf-%{x#MyDAK?#iPfE<(@Q0H7;a125eD%I(+!x1f;Sy`e<9>nm zQH4czZDQmW7^n>jL)@P@aAuAF$;I7JZE5a8~AJI5CNDqyf$gjloKR7C?OPt9yeH}n5 zNF8Vhmd%1O>T4EZD&0%Dt7YWNImmEV{7QF(dy!>q5k>Kh&Xy8hcBMUvVV~Xn8O&%{ z&q=JCYw#KlwM8%cu-rNadu(P~i3bM<_a{3!J*;vZhR6dln6#eW0^0kN)Vv3!bqM`w z{@j*eyzz=743dgFPY`Cx3|>ata;;_hQ3RJd+kU}~p~aphRx`03B>g4*~f%hUV+#D9rYRbsGD?jkB^$3XcgB|3N1L& zrmk9&Dg450mAd=Q_p?gIy5Zx7vRL?*rpNq76_rysFo)z)tp0B;7lSb9G5wX1vC9Lc z5Q8tb-alolVNWFsxO_=12o}X(>@Mwz1mkYh1##(qQwN=7VKz?61kay8A9(94Ky(4V zq6qd2+4a20Z0QRrmp6C?4;%U?@MatfXnkj&U6bP_&2Ny}BF%4{QhNx*Tabik9Y-~Z z@0WV6XD}aI(%pN}oW$X~Qo_R#+1$@J8(31?zM`#e`#(0f<-AZ^={^NgH#lc?oi(Mu zMk|#KR^Q;V@?&(sh5)D;-fu)rx%gXZ1&5)MR+Mhssy+W>V%S|PRNyTAd}74<(#J>H zR(1BfM%eIv0+ngHH6(i`?-%_4!6PpK*0X)79SX0X$`lv_q>9(E2kkkP;?c@rW2E^Q zs<;`9dg|lDMNECFrD3jTM^Mn-C$44}9d9Kc z#>*k&e#25;D^%82^1d@Yt{Y91MbEu0C}-;HR4+IaCeZ`l?)Q8M2~&E^FvJ?EBJJ(% zz1>tCW-E~FB}DI}z#+fUo+=kQME^=eH>^%V8w)dh*ugPFdhMUi3R2Cg}Zak4!k_8YW(JcR-)hY8C zXja}R7@%Q0&IzQTk@M|)2ViZDNCDRLNI)*lH%SDa^2TG4;%jE4n`8`aQAA$0SPH2@ z)2eWZuP26+uGq+m8F0fZn)X^|bNe z#f{qYZS!(CdBdM$N2(JH_a^b#R2=>yVf%JI_ieRFB{w&|o9txwMrVxv+n78*aXFGb z>Rkj2yq-ED<)A46T9CL^$iPynv`FoEhUM10@J+UZ@+*@_gyboQ>HY9CiwTUo7OM=w zd~$N)1@6U8H#Zu(wGLa_(Esx%h@*pmm5Y9OX@CY`3kPYPQx@z8yAgtm(+agDU%4?c zy8pR4SYbu8vY?JX6HgVq7|f=?w(%`m-C+a@E{euXo>XrGmkmFGzktI*rj*8D z)O|CHKXEzH{~iS+6)%ybRD|JRQ6j<+u_+=SgnJP%K+4$st+~XCVcAjI9e5`RYq$n{ zzy!X9Nv7>T4}}BZpSj9G9|(4ei-}Du<_IZw+CB`?fd$w^;=j8?vlp(#JOWiHaXJjB0Q00RHJ@sG6N#y^H7t^&V} z;VrDI4?75G$q5W9mV=J2iP24NHJy&d|HWHva>FaS#3AO?+ohh1__FMx;?`f{HG3v0 ztiO^Wanb>U4m9eLhoc_2B(ca@YdnHMB*~aYO+AE(&qh@?WukLbf_y z>*3?Xt-lxr?#}y%kTv+l8;!q?Hq8XSU+1E8x~o@9$)zO2z9K#(t`vPDri`mKhv|sh z{KREcy`#pnV>cTT7dm7M9B@9qJRt3lfo(C`CNkIq@>|2<(yn!AmVN?ST zbX_`JjtWa3&N*U{K7FYX8})*D#2@KBae` zhKS~s!r%SrXdhCsv~sF}7?ocyS?afya6%rDBu6g^b2j#TOGp^1zrMR}|70Z>CeYq- z1o|-=FBKlu{@;pm@QQJ_^!&hzi;0Z_Ho){x3O1KQ#TYk=rAt9`YKC0Y^}8GWIN{QW znYJyVTrmNvl!L=YS1G8BAxGmMUPi+Q7yb0XfG`l+L1NQVSbe^BICYrD;^(rke{jWCEZOtVv3xFze!=Z&(7}!)EcN;v0Dbit?RJ6bOr;N$ z=nk8}H<kCEE+IK3z<+3mkn4q!O7TMWpKShWWWM)X*)m6k%3luF6c>zOsFccvfLWf zH+mNkh!H@vR#~oe=ek}W3!71z$Dlj0c(%S|sJr>rvw!x;oCek+8f8s!U{DmfHcNpO z9>(IKOMfJwv?ey`V2ysSx2Npeh_x#bMh)Ngdj$al;5~R7Ac5R2?*f{hI|?{*$0qU- zY$6}ME%OGh^zA^z9zJUs-?a4ni8cw_{cYED*8x{bWg!Fn9)n;E9@B+t;#k}-2_j@# zg#b%R(5_SJAOtfgFCBZc`n<&z6)%nOIu@*yo!a% zpLg#36KBN$01W{b;qWN`Tp(T#jh%;Zp_zpS64lvBVY2B#UK)p`B4Oo)IO3Z&D6<3S zfF?ZdeNEnzE{}#gyuv)>;z6V{!#bx)` zY;hL*f(WVD*D9A4$WbRKF2vf;MoZVdhfWbWhr{+Db5@M^A4wrFReuWWimA4qp`GgoL2`W4WPUL5A=y3Y3P z%G?8lLUhqo@wJW8VDT`j&%YY7xh51NpVYlsrk_i4J|pLO(}(b8_>%U2M`$iVRDc-n zQiOdJbroQ%*vhN{!{pL~N|cfGooK_jTJCA3g_qs4c#6a&_{&$OoSQr_+-O^mKP=Fu zGObEx`7Qyu{nHTGNj(XSX*NPtAILL(0%8Jh)dQh+rtra({;{W2=f4W?Qr3qHi*G6B zOEj7%nw^sPy^@05$lOCjAI)?%B%&#cZ~nC|=g1r!9W@C8T0iUc%T*ne z)&u$n>Ue3FN|hv+VtA+WW)odO-sdtDcHfJ7s&|YCPfWaVHpTGN46V7Lx@feE#Od%0XwiZy40plD%{xl+K04*se zw@X4&*si2Z_0+FU&1AstR)7!Th(fdaOlsWh`d!y=+3m!QC$Zlkg8gnz!}_B7`+wSz z&kD?6{zPnE3uo~Tv8mLP%RaNt2hcCJBq=0T>%MW~Q@Tpt2pPP1?KcywH>in5@ zx+5;xu-ltFfo5vLU;2>r$-KCHjwGR&1XZ0YNyrXXAUK!FLM_7mV&^;;X^*YH(FLRr z`0Jjg7wiq2bisa`CG%o9i)o1`uG?oFjU_Zrv1S^ipz$G-lc^X@~6*)#%nn+RbgksJfl{w=k31(q>7a!PCMp5YY{+Neh~mo zG-3dd!0cy`F!nWR?=9f_KP$X?Lz&cLGm_ohy-|u!VhS1HG~e7~xKpYOh=GmiiU;nu zrZ5tWfan3kp-q_vO)}vY6a$19Q6UL0r znJ+iSHN-&w@vDEZ0V%~?(XBr|jz&vrBNLOngULxtH(Rp&U*rMY42n;05F11xh?k;n_DX2$4|vWIkXnbwfC z=ReH=(O~a;VEgVO?>qsP*#eOC9Y<_9Yt<6X}X{PyF7UXIA$f)>NR5P&4G_Ygq(9TwwQH*P>Rq>3T4I+t2X(b5ogXBAfNf!xiF#Gilm zp2h{&D4k!SkKz-SBa%F-ZoVN$7GX2o=(>vkE^j)BDSGXw?^%RS9F)d_4}PN+6MlI8*Uk7a28CZ)Gp*EK)`n5i z){aq=0SFSO-;sw$nAvJU-$S-cW?RSc7kjEBvWDr1zxb1J7i;!i+3PQwb=)www?7TZ zE~~u)vO>#55eLZW;)F(f0KFf8@$p)~llV{nO7K_Nq-+S^h%QV_CnXLi)p*Pq&`s!d zK2msiR;Hk_rO8`kqe_jfTmmv|$MMo0ll}mI)PO4!ikVd(ZThhi&4ZwK?tD-}noj}v zBJ?jH-%VS|=t)HuTk?J1XaDUjd_5p1kPZi6y#F6$lLeRQbj4hsr=hX z4tXkX2d5DeLMcAYTeYm|u(XvG5JpW}hcOs4#s8g#ihK%@hVz|kL=nfiBqJ{*E*WhC zht3mi$P3a(O5JiDq$Syu9p^HY&9~<#H89D8 zJm84@%TaL_BZ+qy8+T3_pG7Q%z80hnjN;j>S=&WZWF48PDD%55lVuC0%#r5(+S;WH zS7!HEzmn~)Ih`gE`faPRjPe^t%g=F ztpGVW=Cj5ZkpghCf~`ar0+j@A=?3(j@7*pq?|9)n*B4EQTA1xj<+|(Y72?m7F%&&& zdO44owDBPT(8~RO=dT-K4#Ja@^4_0v$O3kn73p6$s?mCmVDUZ+Xl@QcpR6R3B$=am z%>`r9r2Z79Q#RNK?>~lwk^nQlR=Hr-ji$Ss3ltbmB)x@0{VzHL-rxVO(++@Yr@Iu2 zTEX)_9sVM>cX$|xuqz~Y8F-(n;KLAfi*63M7mh&gsPR>N0pd9h!0bm%nA?Lr zS#iEmG|wQd^BSDMk0k?G>S-uE$vtKEF8Dq}%vLD07zK4RLoS?%F1^oZZI$0W->7Z# z?v&|a`u#UD=_>i~`kzBGaPj!mYX5g?3RC4$5EV*j0sV)>H#+$G6!ci=6`)85LWR=FCp-NUff`;2zG9nU6F~ z;3ZyE*>*LvUgae+uMf}aV}V*?DCM>{o31+Sx~6+sz;TI(VmIpDrN3z+BUj`oGGgLP z>h9~MP}Pw#YwzfGP8wSkz`V#}--6}7S9yZvb{;SX?6PM_KuYpbi~*=teZr-ga2QqIz{QrEyZ@>eN*qmy;N@FCBbRNEeeoTmQyrX;+ zCkaJ&vOIbc^2BD6_H+Mrcl?Nt7O{xz9R_L0ZPV_u!sz+TKbXmhK)0QWoe-_HwtKJ@@7=L+ z+K8hhf=4vbdg3GqGN<;v-SMIzvX=Z`WUa_91Yf89^#`G(f-Eq>odB^p-Eqx}ENk#&MxJ+%~Ad2-*`1LNT>2INPw?*V3&kE;tt?rQyBw? zI+xJD04GTz1$7~KMnfpkPRW>f%n|0YCML@ODe`10;^DXX-|Hb*IE%_Vi#Pn9@#ufA z_8NY*1U%VseqYrSm?%>F@`laz+f?+2cIE4Jg6 z_VTcx|DSEA`g!R%RS$2dSRM|9VQClsW-G<~=j5T`pTbu-x6O`R z98b;}`rPM(2={YiytrqX+uh65f?%XiPp`;4CcMT*E*dQJ+if9^D>c_Dk8A(cE<#r=&!& z_`Z01=&MEE+2@yr!|#El=yM}v>i=?w^2E_FLPy(*4A9XmCNy>cBWdx3U>1RylsItO z4V8T$z3W-qqq*H`@}lYpfh=>C!tieKhoMGUi)EpWDr;yIL&fy};Y&l|)f^QE*k~4C zH>y`Iu%#S)z)YUqWO%el*Z)ME#p{1_8-^~6UF;kBTW zMQ!eXQuzkR#}j{qb(y9^Y!X7&T}}-4$%4w@w=;w+>Z%uifR9OoQ>P?0d9xpcwa>7kTv2U zT-F?3`Q`7xOR!gS@j>7In>_h){j#@@(ynYh;nB~}+N6qO(JO1xA z@59Pxc#&I~I64slNR?#hB-4XE>EFU@lUB*D)tu%uEa))B#eJ@ZOX0hIulfnDQz-y8 z`CX@(O%_VC{Ogh&ot``jlDL%R!f>-8yq~oLGxBO?+tQb5%k@a9zTs!+=NOwSVH-cR zqFo^jHeXDA_!rx$NzdP;>{-j5w3QUrR<;}=u2|FBJ;D#v{SK@Z6mjeV7_kFmWt95$ zeGaF{IU?U>?W`jzrG_9=9}yN*LKyzz))PLE+)_jc#4Rd$yFGol;NIk(qO1$5VXR)+ zxF7%f4=Q!NzR>DVXUB&nUT&>Nyf+5QRF+Z`X-bB*7=`|Go5D1&h~ zflKLw??kpiRm0h3|1GvySC2^#kcFz^5{79KKlq@`(leBa=_4CgV9sSHr{RIJ^KwR_ zY??M}-x^=MD+9`v@I3jue=OCn0kxno#6i>b(XKk_XTp_LpI}X*UA<#* zsgvq@yKTe_dTh>q1aeae@8yur08S(Q^8kXkP_ty48V$pX#y9)FQa~E7P7}GP_CbCm zc2dQxTeW(-~Y6}im24*XOC8ySfH*HMEnW3 z4CXp8iK(Nk<^D$g0kUW`8PXn2kdcDk-H@P0?G8?|YVlIFb?a>QunCx%B9TzsqQQ~HD!UO7zq^V!v9jho_FUob&Hxi ztU1nNOK)a!gkb-K4V^QVX05*>-^i|{b`hhvQLyj`E1vAnj0fbqqO%r z6Q;X1x0dL~GqMv%8QindZ4CZ%7pYQW~ z9)I*#Gjref-q(4Z*E#1c&rE0-_(4;_M(V7rgH_7H;ps1s%GBmU z{4a|X##j#XUF2n({v?ZUUAP5k>+)^F)7n-npbV3jAlY8V3*W=fwroDS$c&r$>8aH` zH+irV{RG3^F3oW2&E%5hXgMH9>$WlqX76Cm+iFmFC-DToTa`AcuN9S!SB+BT-IA#3P)JW1m~Cuwjs`Ep(wDXE4oYmt*aU z!Naz^lM}B)JFp7ejro7MU9#cI>wUoi{lylR2~s)3M!6a=_W~ITXCPd@U9W)qA5(mdOf zd3PntGPJyRX<9cgX?(9~TZB5FdEHW~gkJXY51}?s4ZT_VEdwOwD{T2E-B>oC8|_ZwsPNj=-q(-kwy%xX2K0~H z{*+W`-)V`7@c#Iuaef=?RR2O&x>W0A^xSwh5MsjTz(DVG-EoD@asu<>72A_h<39_# zawWVU<9t{r*e^u-5Q#SUI6dV#p$NYEGyiowT>>d*or=Ps!H$-3={bB|An$GPkP5F1 zTnu=ktmF|6E*>ZQvk^~DX(k!N`tiLut*?3FZhs$NUEa4ccDw66-~P;x+0b|<!ZN7Z%A`>2tN#CdoG>((QR~IV_Gj^Yh%!HdA~4C3jOXaqb6Ou z21T~Wmi9F6(_K0@KR@JDTh3-4mv2=T7&ML<+$4;b9SAtv*Uu`0>;VVZHB{4?aIl3J zL(rMfk?1V@l)fy{J5DhVlj&cWKJCcrpOAad(7mC6#%|Sn$VwMjtx6RDx1zbQ|Ngg8N&B56DGhu;dYg$Z{=YmCNn+?ceDclp65c_RnKs4*vefnhudSlrCy6-96vSB4_sFAj# zftzECwmNEOtED^NUt{ZDjT7^g>k1w<=af>+0)%NA;IPq6qx&ya7+QAu=pk8t>KTm` zEBj9J*2t|-(h)xc>Us*jHs)w9qmA>8@u21UqzKk*Ei#0kCeW6o z-2Q+Tvt25IUkb}-_LgD1_FUJ!U8@8OC^9(~Kd*0#zr*8IQkD)6Keb(XFai5*DYf~` z@U?-{)9X&BTf!^&@^rjmvea#9OE~m(D>qfM?CFT9Q4RxqhO0sA7S)=--^*Q=kNh7Y zq%2mu_d_#23d`+v`Ol263CZ<;D%D8Njj6L4T`S*^{!lPL@pXSm>2;~Da- zBX97TS{}exvSva@J5FJVCM$j4WDQuME`vTw>PWS0!;J7R+Kq zVUy6%#n5f7EV(}J#FhDpts;>=d6ow!yhJj8j>MJ@Wr_?x30buuutIG97L1A*QFT$c ziC5rBS;#qj=~yP-yWm-p(?llTwDuhS^f&<(9vA9@UhMH2-Fe_YAG$NvK6X{!mvPK~ zuEA&PA}meylmaIbbJXDOzuIn8cJNCV{tUA<$Vb?57JyAM`*GpEfMmFq>)6$E(9e1@W`l|R%-&}38#bl~levA#fx2wiBk^)mPj?<=S&|gv zQO)4*91$n08@W%2b|QxEiO0KxABAZC{^4BX^6r>Jm?{!`ZId9jjz<%pl(G5l));*`UU3KfnuXSDj2aP>{ zRIB$9pm7lj3*Xg)c1eG!cb+XGt&#?7yJ@C)(Ik)^OZ5><4u$VLCqZ#q2NMCt5 z6$|VN(RWM;5!JV?-h<JkEZ(SZF zC(6J+>A6Am9H7OlOFq6S62-2&z^Np=#xXsOq0WUKr zY_+Ob|CQd1*!Hirj5rn*=_bM5_zKmq6lG zn*&_=x%?ATxZ8ZTzd%biKY_qyNC#ZQ1vX+vc48N>aJXEjs{Y*3Op`Q7-oz8jyAh>d zNt_qvn`>q9aO~7xm{z`ree%lJ3YHCyC`q`-jUVCn*&NIml!uuMNm|~u3#AV?6kC+B z?qrT?xu2^mobSlzb&m(8jttB^je0mx;TT8}`_w(F11IKz83NLj@OmYDpCU^u?fD{) z&=$ptwVw#uohPb2_PrFX;X^I=MVXPDpqTuYhRa>f-=wy$y3)40-;#EUDYB1~V9t%$ z^^<7Zbs0{eB93Pcy)96%XsAi2^k`Gmnypd-&x4v9rAq<>a(pG|J#+Q>E$FvMLmy7T z5_06W=*ASUyPRfgCeiPIe{b47Hjqpb`9Xyl@$6*ntH@SV^bgH&Fk3L9L=6VQb)Uqa z33u#>ecDo&bK(h1WqSH)b_Th#Tvk&%$NXC@_pg5f-Ma#7q;&0QgtsFO~`V&{1b zbSP*X)jgLtd@9XdZ#2_BX4{X~pS8okF7c1xUhEV9>PZco>W-qz7YMD`+kCGULdK|^ zE7VwQ-at{%&fv`a+b&h`TjzxsyQX05UB~a0cuU-}{*%jR48J+yGWyl3Kdz5}U>;lE zgkba*yI5>xqIPz*Y!-P$#_mhHB!0Fpnv{$k-$xxjLAc`XdmHd1k$V@2QlblfJPrly z*~-4HVCq+?9vha>&I6aRGyq2VUon^L1a)g`-Xm*@bl2|hi2b|UmVYW|b+Gy?!aS-p z86a}Jep6Mf>>}n^*Oca@Xz}kxh)Y&pX$^CFAmi#$YVf57X^}uQD!IQSN&int=D> zJ>_|au3Be?hmPKK)1^JQ(O29eTf`>-x^jF2xYK6j_9d_qFkWHIan5=7EmDvZoQWz5 zZGb<{szHc9Nf@om)K_<=FuLR<&?5RKo3LONFQZ@?dyjemAe4$yDrnD zglU#XYo6|~L+YpF#?deK6S{8A*Ou;9G`cdC4S0U74EW18bc5~4>)<*}?Z!1Y)j;Ot zosEP!pc$O^wud(={WG%hY07IE^SwS-fGbvpP?;l8>H$;}urY2JF$u#$q}E*ZG%fR# z`p{xslcvG)kBS~B*^z6zVT@e}imYcz_8PRzM4GS52#ms5Jg9z~ME+uke`(Tq1w3_6 zxUa{HerS7!Wq&y(<9yyN@P^PrQT+6ij_qW3^Q)I53iIFCJE?MVyGLID!f?QHUi1tq z0)RNIMGO$2>S%3MlBc09l!6_(ECxXTU>$KjWdZX^3R~@3!SB zah5Za2$63;#y!Y}(wg1#shMePQTzfQfXyJ-Tf`R05KYcyvo8UW9-IWGWnzxR6Vj8_la;*-z5vWuwUe7@sKr#Tr51d z2PWn5h@|?QU3>k=s{pZ9+(}oye zc*95N_iLmtmu}H-t$smi49Y&ovX}@mKYt2*?C-i3Lh4*#q5YDg1Mh`j9ovRDf9&& zp_UMQh`|pC!|=}1uWoMK5RAjdTg3pXPCsYmRkWW}^m&)u-*c_st~gcss(`haA)xVw zAf=;s>$`Gq_`A}^MjY_BnCjktBNHY1*gzh(i0BFZ{Vg^F?Pbf`8_clvdZ)5(J4EWzAP}Ba5zX=S(2{gDugTQ3`%!q`h7kYSnwC`zEWeuFlODKiityMaM9u{Z%E@@y1jmZA#ⅅ8MglG&ER{i5lN315cO?EdHNLrg? zgxkP+ytd)OMWe7QvTf8yj4;V=?m172!BEt@6*TPUT4m3)yir}esnIodFGatGnsSfJ z**;;yw=1VCb2J|A7cBz-F5QFOQh2JDQFLarE>;4ZMzQ$s^)fOscIVv2-o{?ct3~Zv zy{0zU>3`+-PluS|ADraI9n~=3#Tvfx{pDr^5i$^-h5tL*CV@AeQFLxv4Y<$xI{9y< zZ}li*WIQ+XS!IK;?IVD0)C?pNBA(DMxqozMy1L#j+ba1Cd+2w&{^d-OEWSSHmNH>9 z%1Ldo(}5*>a8rjQF&@%Ka`-M|HM+m<^E#bJtVg&YM}uMb7UVJ|OVQI-zt-*BqQ zG&mq`Bn7EY;;+b%Obs9i{gC^%>kUz`{Qnc=ps7ra_UxEP$!?f&|5fHnU(rr?7?)D z$3m9e{&;Zu6yfa1ixTr;80IP7KLgkKCbgv1%f_weZK6b7tY+AS%fyjf6dR(wQa9TD zYG9`#!N4DqpMim|{uViKVf0B+Vmsr7p)Y+;*T~-2HFr!IOedrpiXXz+BDppd5BTf3 ztsg4U?0wR?9@~`iV*nwGmtYFGnq`X< zf?G%=o!t50?gk^qN#J(~!sxi=_yeg?Vio04*w<2iBT+NYX>V#CFuQGLsX^u8dPIkP zPraQK?ro`rqA4t7yUbGYk;pw6Z})Bv=!l-a5^R5Ra^TjoXI?=Qdup)rtyhwo<(c9_ zF>6P%-6Aqxb8gf?wY1z!4*hagIch)&A4treifFk=E9v@kRXyMm?V*~^LEu%Y%0u(| z52VvVF?P^D<|fG)_au(!iqo~1<5eF$Sc5?)*$4P3MAlSircZ|F+9T66-$)0VUD6>e zl2zlSl_QQ?>ULUA~H?QbWazYeh61%B!!u;c(cs`;J|l z=7?q+vo^T#kzddr>C;VZ5h*;De8^F2y{iA#9|(|5@zYh4^FZ-3r)xej=GghMN3K2Y z=(xE`TM%V8UHc4`6Cdhz4%i0OY^%DSguLUXQ?Y3LP+5x3jyN)-UDVhEC}AI5wImt; zHY|*=UW}^bS3va-@L$-fJz2P2LbCl)XybkY)p%2MjPJd-FzkdyWW~NBC@NlPJkz{v z+6k6#nif`E>>KCGaP34oY*c#nBFm#G8a0^px1S6mm6Cs+d}E8{J;DX=NEHb|{fZm0 z@Ors@ebTgbf^Jg&DzVS|h&Or)56$+;%&sh0)`&6VkS@QxQ=#6WxF5g+FWSr7Lp9uF zV#rc`yLe?f*u6oZoi3WpOkKFf^>lHb2GC6t!)dyGaQbK7&BNZ7oyP)hUX1Y(LdW-I z6LI2$i%+g!zsjT(5l}5ROLb)8`9kkldbklcq6tfLSrAyh#s(C1U2Sz9`h3#T9eX#Hryi1AU^!uv*&6I~qdM_B7-@`~8#O^jN&t7+S zTKI6;T$1@`Kky-;;$rU1*TdY;cUyg$JXalGc&3-Rh zJ&7kx=}~4lEx*%NUJA??g8eIeavDIDC7hTvojgRIT$=MlpU}ff0BTTTvjsZ0=wR)8 z?{xmc((XLburb0!&SA&fc%%46KU0e&QkA%_?9ZrZU%9Wt{*5DCUbqIBR%T#Ksp?)3 z%qL(XlnM!>F!=q@jE>x_P?EU=J!{G!BQq3k#mvFR%lJO2EU2M8egD?0r!2s*lL2Y} zdrmy`XvEarM&qTUz4c@>Zn}39Xi2h?n#)r3C4wosel_RUiL8$t;FSuga{9}-%FuOU z!R9L$Q!njtyY!^070-)|#E8My)w*~4k#hi%Y77)c5zfs6o(0zaj~nla0Vt&7bUqfD zrZmH~A50GOvk73qiyfXX6R9x3Qh)K=>#g^^D65<$5wbZjtrtWxfG4w1f<2CzsKj@e zvdsQ$$f6N=-%GJk~N7G(+-29R)Cbz8SIn_u|(VYVSAnlWZhPp8z6qm5=hvS$Y zULkbE?8HQ}vkwD!V*wW7BDBOGc|75qLVkyIWo~3<#nAT6?H_YSsvS+%l_X$}aUj7o z>A9&3f2i-`__#MiM#|ORNbK!HZ|N&jKNL<-pFkqAwuMJi=(jlv5zAN6EW`ex#;d^Z z<;gldpFcVD&mpfJ1d7><79BnCn~z8U*4qo0-{i@1$CCaw+<$T{29l1S2A|8n9ccx0!1Pyf;)aGWQ15lwEEyU35_Y zQS8y~9j9ZiByE-#BV7eknm>ba75<_d1^*% zB_xp#q`bpV1f9o6C(vbhN((A-K+f#~3EJtjWVhRm+g$1$f2scX!eZkfa%EIZd2ZVG z6sbBo@~`iwZQC4rH9w84rlHjd!|fHc9~12Il&?-FldyN50A`jzt~?_4`OWmc$qkgI zD_@7^L@cwg4WdL(sWrBYmkH;OjZGE^0*^iWZM3HBfYNw(hxh5>k@MH>AerLNqUg*Og9LiYmTgPw zX9IiqU)s?_obULF(#f~YeK#6P>;21x+cJ$KTL}|$xeG?i`zO;dAk0{Uj6GhT-p-=f zP2NJUcRJ{fZy=bbsN1Jk3q}(!&|Fkt_~GYdcBd7^JIt)Q!!7L8`3@so@|GM9b(D$+ zlD&69JhPnT>;xlr(W#x`JJvf*DPX(4^OQ%1{t@)Lkw5nc5zLVmRt|s+v zn(25v*1Z(c8RP@=3l_c6j{{=M$=*aO^ zPMUbbEKO7m2Q$4Xn>GIdwm#P_P4`or_w0+J+joK&qIP#uEiCo&RdOaP_7Z;PvfMh@ zsXUTn>ppdoEINmmq5T1BO&57*?QNLolW-8iz-jv7VAIgoV&o<<-vbD)--SD%FFOLd z>T$u+V>)4Dl6?A24xd1vgm}MovrQjf-@YH7cIk6tP^eq-xYFymnoSxcw}{lsbCP1g zE_sX|c_nq(+INR3iq+Oj^TwkjhbdOo}FmpPS2*#NGxNgl98|H0M*lu)Cu0TrA|*t=i`KIqoUl(Q7jN zb6!H-rO*!&_>-t)vG5jG>WR6z#O9O&IvA-4ho9g;as~hSnt!oF5 z6w(4pxz|WpO?HO<>sC_OB4MW)l`-E9DZJ$!=ytzO}fWXwnP>`8yWm5tYw`b1KDdg zp@oD;g===H+sj+^v6DCpEu7R?fh7>@pz>f74V5&#PvBN+95?28`mIdGR@f*L@j2%% z%;Rz5R>l#1U zYCS_5_)zUjgq#0SdO#)xEfYJ)JrHLXfe8^GK3F*CA(Y)jsSPJ{j&Ae!SeWN%Ev727 zxdd3Y0n^OBOtBSKdglEBL)i5=NdKfqK=1n~6LX`ja;#Tr!II$AAH{Z#sp%`rwNGT5 zvHT%(LJB+kD{5N}7c_Rk6}@tikIeq%@MqxX%$P!(238YD(H<_d;xxo*oMiv^1io>g zt5z&6`}cjci90q2r0hutQXr!UA~|4e*u=k81D(Cp7n{4LVCa+u0%-8Uha+sqI#Om~ z!&)KN(#Zone^~&@Ja{|l?X64Dxk)q>tLRv{=0|t$`Kdaj z#{AJr>{_BtpS|XEgTVJ4WMvBRk-(mk@ZYGdY1VwI z81;z(MBGV|2j*Cj%dvl8?b2{{B#e0B7&7wfv+>g`R2^Ai5C_WUx|CnTrHm+RFGXrt zs<~zBtk@?Niu%|o6IEL+y60Q>zJlv``ePCa07C%*O~lj?74|}&A0!uA)3V7ST8b_- z6CBP1;x+S@xTzgOY2#s%@=bhZ@i@BwmS)neQG&=9KUtRf^K=MvjC5JnqLqykCE_P0 zjf#V4SdH2#%2EuDb!>FLHK7j;nd6VLW|$3gJuegpEl3DZ`BpJU$<}}A(rW?<6OB@9 zKP9G3An?T5BztrLdlximA;{>Tr7GAeSU=^<*y;%RHj+7;v+tonyh(8d;Izn}2{oz& zW)fsZ9gHYpI?B|uekS3zHUue3mI zb7?0+&Zm>Kq(F>~%VYEn)0b32I3~O^?Wx-HI|Zu?1-OA2yfyJ;gWygLOeU;)vRm3u z5J4vDIQYztnEm=QauX2(WJO{yzI0HUFl+oO&isMf!Yh2pu@p}65)|0EdWRbg(@J6qo5_Els>#|_2a1p0&y&UP z8x#Z69q=d663NPPi>DHx3|QhJl5Ka$Cfqbvl*oRLYYXiH>g8*vriy!0XgmT~&jh3l z+!|~l=oCj<*PD>1EY*#+^a{rVk3T(66rJ^DxGt|~XTNnJf$vix1v1qdYu+d@Jn~bh z!7`a`y+IEcS#O*fSzA;I`e_T~XYzpW7alC%&?1nr);tSkNwO&J`JnX+7X1Q8fRh_d zx%)Xh_YjI3hwTCmGUeq_Z@H#ovkk_b(`osa$`aNmt`9A#t&<^jvuf z1E1DrW(%7PpAOQGwURz@luEW9-)L!`Jy*aC*4mcD?Si~mb=3Kn#M#1il9%`C0wkZ` zbpJ-qEPaOE5Y5iv_z%Wr{y4jh#U+o^KtP{pPCq-Qf&!=Uu)cEE(Iu9`uT#oHwHj+w z_R=kr7vmr~{^5sxXkj|WzNhAlXkW^oB4V)BZ{({~4ylOcM#O>DR)ZhD;RWwmf|(}y zDn)>%iwCE=*82>zP0db>I4jN#uxcYWod+<;#RtdMGPDpQW;riE;3cu``1toL|FaWa zK)MVA%ogXt3q55(Q&q+sjOG`?h=UJE9P;8i#gI*#f}@JbV(DuGEkee;La*9{p&Z?;~lE!&-kUFCtoDHY*MS zzj+S$L9+aTs(F^4ufZe6>SBg;m@>0&+kEZMFmD*~p~sx?rx=!>Ge;KYw<33y#*&77 zFZI`YE(Iz?+tH;Fq;y=MaSqT{Ayh*HFv0(z{_?Q+7@nE%p?S8%X6c!+y;!0NLXwJV8Co_}R3*7>n+oMsQpv8}8ZS-P@(Rg|gmxZHzf=nMOUAAY}AZGfWVzZjE@4$=7xkIrs8BE%606aVU%kxz_04ipig51k& z(>c9rJL2q%xvU%Zj#GR9C9)HLCR;#zQBB@x;e_9$ayn(JmSg_*0G?+wOF?&iu@}S{ zt$;TPf*Lj$3=d<}Q3o!Hq@3~lFxoiCyeEt}o3fihIn{x2s1)e2@3##&GYDq~YO|!q zUs0P-zy)+ohl-VQ`bhvUpC{-d$lkpML_M%Kl6@#_@A}w{jWCDsPa#cSbWA#C4Sf|*C*&Z{ zz?hOU7Cc`?>H$WGqITA2P~fYudnQHxB8^;0ZFKC;19F#~n_2P@{cE{Czq-#K5L_8| zc3aOEwq4%zL5>YU_mc9fc-p~{fBTWUkxTiZvxt9FOqC{s#TBp(#dWc+{Ee{dZ#B!g zHnaOJ8;KO1G;QU2ciodE+#Z$Wuz*Hc6NRO!AUMi|gov=>=cwcZeL&`>Jfn!35hV1J z;B2@0!bIR853w%T*m6)gQ?DPnQ)o6EtKaN3L;o?*q<83d&lG&U=A|6hcT?f0)4h6{ zGIZ0|!}-?*n{zr}-}cC}qWxEN%g60+{my)o^57{QEn(tSrmD7o)|r0+HVpQPopFu; z0<S}pW8W2vXzSxEqGD+qePj^x?R$e2LO&*ewsLo{+_Z)Wl|Z1K47j zsKoNRlX)h2z^ls_>IZ0!2X5t&irUs%RAO$Dr>0o$-D+$!Kb9puSgpoWza1jnX6(eG zTg-U z6|kf1atI!_>#@|=d01Ro@Rg)BD?mY3XBsG7U9%lmq>4;Gf&2k3_oyEOdEN&X6Hl5K zCz^hyt67G;IE&@w1n~%ji_{sob_ssP#Ke|qd!Xx?J&+|2K=^`WfwZ-zt|sklFouxC zXZeDgluD2a?Zd3e{MtE$gQfAY9eO@KLX;@8N`(?1-m`?AWp!a8bA%UN>QTntIcJX zvbY+C-GD&F?>E?jo$xhyKa@ps9$Dnwq>&)GB=W~2V3m)k;GNR$JoPRk%#f3#hgVdZ zhW3?cSQ*((Fog26jiEeNvum-6ID-fbfJ?q1ZU#)dgnJ^FCm`+sdP?g;d4VD$3XKx{ zs|Y4ePJp|93fpu)RL+#lIN9Ormd;<_5|oN!k5CENnpO>{60X;DN>vgHCX$QZYtgrj z*1{bEA1LKi8#U%oa!4W-4G+458~`5O4S1&tuyv>%H9DjLip7cC~RRS@HvdJ<|c z$TxEL=)r)XTfTgVxaG!gtZhLL`$#=gz1X=j|I@n~eHDUCW39r=o_ml@B z0cDx$5;3OA2l)&41kiKY^z7sO_U%1=)Ka4gV(P#(<^ z_zhThw=}tRG|2|1m4EP|p{Swfq#eNzDdi&QcVWwP+7920UQB*DpO0(tZHvLVMIGJl zdZ5;2J%a!N1lzxFwAkq05DPUg2*6SxcLRsSNI6dLiK0&JRuYAqwL}Z!YVJ$?mdnDF z82)J_t=jbY&le6Hq$Qs}@AOZGpB1}$Ah#i;&SzD1QQNwi6&1ddUf7UG0*@kX?E zDCbHypPZ9+H~KnDwBeOXZ-W-Y80wpoGB*A) z_;26Z`#s0tKrf~QBi2rl2=>;CS1w)rcD3-sB!8NI*1iQo59PJ>OLnqeV4iK7`RBi^ zFW{*6;nlD&cSunmU3v4JKj|K4xeN(q>H%;SsY8yDdw5BJ75q8>Ov)&D5OPZ`XiRHl z;)mAA0Woy6f!xCK(9H2rq?qzp83liZAIpBPl-dQ&$2=&H?Im~%g;vnIw1I+8q|kr! z36&^9}CMmR(U2rf|j12oG=vb%Ypsq8u9Kq}U*ANX*)9uK}fAi8;V_7Z;0_4*iydDxN-? zv?qJ=T*{MzL~-xUv{_Kh_q9#F{8gPV!yPUUS8pEq*=}2-#1d=sC_|U-rX~F0 zBLawgCWy#?#ax{~DAnDvh^`}wyUO`ioMK~jgh%L7^}#h?beSyvQ_g>+`2`}`-1h7# zg*?qJdm=53hwN8~B=^|LPmYtOVrQ(W{sNm4uofq=4P@dUA%$onWbw_m-KWia&n9iv zi)!9#OJ#^}eg8tE{wSb9(c0D^PS1 z9EBS5*ypSiVRS_G0v?$hyoZOS7hFWlp4qbYkf9Y&{%OzhsIdHskLptn96@k6@^K@U zszd8POehITDK+AyW#JKpnWY;ju#MC$JjB1Y*~(E6N%{p#kO+bVxG3X<34n3fW=k{A zCZt|KP%x^GQ9%mU)KE0{LA=vaZvRQbxSlK~eAkwWo2Z<{j5eS5NVTMe`m%re8%~7K zZLtU&b~YDN%~uA9wPf>x2=PI=MA6_oVe>Ek$s5&&Z=8vvF5EODP4Av(b|dlNgF1O8 zy83W0WRdzjz2iNA~t1piEqlyU&`$yZtqR`6X_PmuP>W+D|8iH;FQ zN{JuU#Tz9mV=4R_IewROL1|mK^`lLat#LcIBfggzM(iO$pQT*-c_ z94^LUWw#5B9~sp2W1p`c)Y(xfR<{O^9n4E6vDDw{#-R4UMBKo{>Hqlqn*a9rl_>+0 zS5MwJC~nCC`1X%VCyWFsiDX;bfAJQAUkU#105f_s5U-8rqO}n8fA1{b>Fr6Q|Ea(V z5B11Lo^ooWF?`^{-U#?iatokWI-e$632frzY?Yzzx(xJc@LFM4A~-eg!u|tl{)8Nx ztZLXsSC*68g%9TFu(f&J9nmc^9hgyy#uUOMJFCaifSaDcyQ&6=8e9=t zIFEAQ{EK{|73{($!a4=!wj4ABcQrUQp#+gGM?wEUp(w@+Fzi{!lt}|3`PM%&d-seeR zB$}BrFGD3R10CE>Hsb>;PrP}pd` zaY4}6+Wu(`#uAV+E5SV7VIT7ES#b(U0%%DgN1}USJH>)mm;CHPv>}B18&0F~Kj@1= z&^Jyo+z-E)GRT4U*7$8wJO1OibWg0Jw>C$%Ge|=YwV@Y1(4fR>cV#6aGtRoF@I`*w_V4;)V231NzNqb6g@jdpjmjv*<2j02yU$F8ZS$fTvCC`%|Yn#x< zXUnP&b!GLpOY-TY3d?<-Hhxom_LM9`JC9LEX2{t1P-Nj%nG+0Vq)vQwvO^}coPH-> zAo8w#s>Je^Yy*#PlK=XDxpVS~pFe-j#jN-(As&LRewOf(kN-aKF(H+s*{*!0xrlZw zchJu@XAvQWX7DI1E8?F}Wc8m46eT+C<0eXVB+Z^(g=Kl@FG-cn@u$suj)1V2(KNg_ zh29ws6&6(q~+sOAoHY^o86A<#n*?Pg2)cK$+y;cY$hJLq4)4V84=j+3ShSr##Tk5kgmxB zkW+8A1GtceEx~^Ebhwm36U?oA)h)!mt=eg0QE$D1QsLNZ_T3NH?=B&0j~#298!6iv zhc0|-{46*3`Rx&nKSXnf1&w-Rs>#PGAGuY@cBTU-j|Fxbn3z49S#6KBaP^Lx*AOXxIibr z!1ysMi(&kr!1wwQB5w`BDH2~>T4bI`T1}A2RM0zd7ikC&kuBRsB`Z2@J!Udm{AmSN zrr0k6_qCZL**=)xRW`MFu(OY=OT;3G8eF~ z2mmkXZ9X(sjuKmq+_<=LSjphB$~R1o^Yb=rO!j!(4ErIox^x55o{pXSE9X$!76^*$ zoKhlAX6y%n^U=C~@!vIlEgXQGD@>oOU=_(aXF-Sjas*$AKESfRzxQ8#3yOj|y0OCU z>6Z-0%LCcjla&7I+CXm&caKp@@jQ!5M`(_{CL=@4#JJ}cHeZw>^b6fpv269LSV?gV5Q{kk?4;;y9RIsy5vk%DIRiL(9xe1aA@4!VX zDh2}xgUd5X?6nji%&7-%QuyKSYA-Z{PwJijUQ}In+EJl|x@dF1P<5bPa5W3&&?^h$ zZCo8LepKo0a(Fsln*cHL;D(gu9MMkoiM0*n31u)jHqX5x^F95tnI&^}^yKx3YwEm@ zo8?EZ710ykx@19{=yz5IXb8w4yjdveWb{IVL6Z(Cs>!a_0X^1E27o!4e&b43+J*u2Gb(59k2uK0goLwhO{ujLS ziI9LA9`&x~Y$6JNX!aEXR``}LUI}Gr#=<^wBHmg%v<)zRWDVtq)kT$-P7iU1R)2XZ zi~bYhV@EZ`@prgK(cs{>2jn$pxg$<|KjJ7%26Km>%KcXh^bU@y@V_Lf@=j1x%R4{v zOcQn{I}!2W<~08FOVnoV>zOTH=+>v9!jFo|q)ucqIe!N4{U5_G`>>*sVD{8I~4FqyU8imZ**-Gy`~Xd z4w35GMf%7^i65HdX{Iz|f2Kg193#KhPIeR)-=eYx3Z!%RM=JjwLrdk^B#6rg!ym2w zPbFqYyO4>W_Z6PonAwiu7?!h=x%sR-T+_*xZOGh2wWhWr%}%2^$$ zQvACIB~pi=m|`hXIMvoq`TOCx=J_D2>pi6$NPy3&8#vy|oX)=kM0Z}$BR$r0G}MzOk-OqG+VmZtOZoj6x4(tLh|5h) zBv64Y{DPHsy&_H(5_l(&Y}FhVvr9m_*_Q~Zy-}V9+VmGnvndEjYW4qt4K~N&Y&6g| zfpz*V=A#^mVmuOAz)(KVI<%v5NY0%Goy!{9&o41upsPWk(yFuRP|A4q6NMnX%V~MT zi_Rb-Bno2kI+j0Cw`@ydy{e%ARS#Z%b6I%_yfo_ZKXr4BLVoHzBKJ^ZG z-2>2IzU)55@9C|?_P$ew^-7zEiAKG1XAi{!3h%1m#9s%^pGy6S9wKFYY4<$djeoJP z{GI}Vd%idY$4_fh(7NXm7#;cC!DS&-{tGr!Qze{^%bUx2jgG@-kMta^q-EwrKB}d8 z{%FT>rFk_bzW<{lc%eYlrsiYTZXGgzD1&lmRyp+c1O=0=zAX=KV62bx-a~JP{cPF4 zU$-XT#(9&T>l@bMu3nSr{)%-5lV+0t&bxip4DVJ~vlL$J2P6X~ zd{FS8vm{Lhrieul*7&(AgPuXhjpGila%6_?-+k#b)cdk#M1jB*nE>G6NGOr+Ek{`= z9b%S1`$`=g0CC$>0$Db;l_szReLYVmce*(()9%Zz1`*fNXhI*oRlerWHarD(v^W^c zuc1Vuw6Gbp7ZsoRH>QGt#&lv;5G~Ovt$%7VFd*-rN2>UjbOWBFGNGO`bru7CFB4tn zL`^?69Lj_g_TA&`9`dSI8s|)K|QM0 zybvV7!>xDY|6c6y;Q}qs`){1+WQu_5Dgd8Qe|q}}bxjH+joQQtqs1IVZn6{e7T{ia zF|=^xa%eWO%(x<7j*QZbcU_;aVaVP!arexOLOtoSNt*hvsRL%}%)jPetSich(`b-^ zMZ$PM9%s@%*jPVz0Z^W*cK_>G4f}+eEVX`HOaHg#!B`<4v;x}zDLMR*M27`kNfp!! zOfdt(>k-g>7jf^{Se@3$8<+;R*cYtw+wD_Z8Pl~!JDCUEPq{Ea*!J9`%ihyNJZ30i zmfve}S5<$Uso}_?SuI$ks|{-ddGLu9WR9`^9)Kdi@Vs;x#SY-xp}wHPU0|vEA7234 z@BN1z7OF=OOQtPF$4twn3!HTVlUVD_)ubMM7PEPoiC6lQgL2q9PK4~e8v-OuH%lie z?NgBLkIdPMG$QBq(>r^AOHB`|*1#*!2Z? zuU8H|FD`OBRu^(R?Z-Vhr0j;FLpS~a34KREnd}B=EYHS*>Hm+f%tgJt!4J8Q`qn^4 z9F=tO#JRJ}tzA`vx$nZ)O%wC?Uiv0+_nz}5Lj4ki*&=K&*#U`=rv z`Q@Q{+IhAj@6lrNK2B=8Yln!O2%zomfRehFT~;!O@(@Xy|1Jlw*uOB-M$#6K^)QBm z_7%#QVUDPwnW{iOV-grMQQU|3{=BQMh}c5(yMGdoQf*)k9-B zMQ(^GdJh+y)>qJprknS!%WxqM>HlHOP#7UVdy>%PW$!l72J`n-p7j(DBKoGxXWh(Y z>BFDZl|7knU_jg_SSbvFk8)39%2)Hu5W0}HKlh>EaqvFoXI&56Yy)3) zQkE4X^P0QnPn?iUUVHJZXzPp`s5uv?pG{K9IgGoHvcmlBxubi|iF7n{)mhenIcxGs zgr0OpQy#Y#u=5lOyiECfE_Sn?Fj1LyoRKcbTgX{p<T*v!CGkPc)pcA2D=4Ekp0Gb*wpy7S88C%Ywsbr?MI(3UdsCM?XJ1X%*hNjB)XqZ*W(qDdtSb z<3XN74ARXL3=c^bfW~F%NM^5*Zx92>Wq`&M625p~j$8mYwLbk%Kf)jbn#<2z$%vP5 zy#b>-tF-S2_AB4;R^K&^-1LJrUmi@9rB^FLF)-k&YHK8P+k@RCJ1qSTZ@=kHxA3l$ zmK_ZG)l6(nmCR1a8|;QF-B5e_ELnjJ1$m-;4UXX?WytF_wz7#&AjwZYTMVieLbq@R z3t-q|G4^BB#EpNu4uyfDebB+-uu_$9>y-dzB30Y9F=R zrW-Heqnj*InPTWHgR9v^R7~hokldh&h8=HDhMW(EFfim1*{)5Lc1-+eBVkK-2!u=N zuZKABgJs3I--NbjE;>Undg6uK`^U>AQ6V zhc!RhYgvrmeGNsftr+(C<_MtuV$`5RZTf#5r=DR?gWG->#})#=(td%C3`oO+2B7im zUqY}&a_QNTn?s+?=mNXiREN%x_=(H)L|DtYPY>SR3pQfBOel7G_jR_{!9`dSj8Up-`JgcB;=Oor)U=_EVjF3C5{Sqh8cq=~bRjoBpoc$kJCgtTyZGSpQ4= zYi$6b$-dGmuTDF&@amhV?cU05g(AZV&v2$4m&j_~GZk;&keSO(@LRESRZ&p`dV*6w z2$em~p*8yM6j;SYorw`M5K2mluJq7P5Yn$VtZj8DEs2Zk=O@4T&Q}>~f31Z{uk}`E z{Dp{KObh1kk~~MfLUod72{Pk6G@T$_0_N??lOrdR=Z;VV#m0l)&@hz{Z?)@sgImi-&i1@95g53rON83v!yVPDHRU*Mzc4yZ(-Fr z{8{WXmIJf7jeswk$;6s~Qac6QyM3W&`}m#gRt=rr95A+Ad&wSAgvXZ|F))rBJVJ5W1CsjN`QaOzct2ocq#0!v zmj#075)C!3oS>&N;aHS@<+c>RHL)8j^p)k(8#7$LEx!1g_1^02!4_qA=;uhKW=+ix zGX%+vBMiRiF^^jm{mdO(?GdWJ#unO#_F^7mhT8)s(z_WlwFyJ#Xh)k5+RG2f;LC*K**1dr`#}~6A=0B=I&V;%zDA1)d@G!X#Rng)7G*2k8Kg447r0ox> z5NK`d(H-afBwo9feDOUi>;BbPsu!2|=@g=3j*PY}@YrOb+SX6?#Yb2xaaK!?>SX1J z_!VsB`2n1=wwSftkydm!39|-1?c%Epx?TO<(#GO~I&{f4+)XwRk<7RQ1~5>QcKH|D z?!}j1ueO0Lk;FZ{k4FA_(S`Ot0w~tl&m0duID*f6RY#bkw||o;kZ# zISYNTb|{~|X$m$Q-Jv#uxyw)eM0gIv`V#wOAp&Vv@>X4_tSZ&L#juM@$S9 zx_X_tLh<_^-F;LAQ09s@sPb%PMTrcw*HUV0P=RYSlM&AXEOI&&R&YCm_S<7DRBx^L zA^R^iwW+LMk(r*$Pq-fKU5X@=mQ=`ErO30H@@&qqnI7zJcrbSh+H<V ze&7Uli0xj@WrW#&-9%*FP~kPYF_YYM_hs5~|ExMynQ%qvq`leRB6W0yhC@pCb8>_P zlf=F~WMv_u*-DV=UaVu#2rlzK{q8D95VwZrfV?gj@rSNWXFvktUq)V5+YrlxwX302ae(;aG4e>L-M@3J+-f3IT{b9l!kg*2M zC1+ND9}6m^()LE87Mt+^Q|)!y#suc&v26C=0W88%a{?)E8Yvo@kM&KNMaOst#|-_CbUTm}WS@-c>nRb;&z^ zYr)+IE$1=jov(CZ%3uR+`~NI>1&Gs6W(jaamjcN$a`2!*nO}l|b%?)Q%%UWzw>A`C zR@px(P*7j$TK?jbv*%x)e^|jcLsv}aF(Z0=7(%Oa7+1wY>{B>d+i&ZA$}k(qgZPZY z;VkW~8eWnU&HPIAbco?&tc2O1$6=7n{u|^Y*nXoac{o1W-6aXfy~KlNbJfLoq~6;+ zDYmnv--Fhqrl+UV#k@_(1=gWNtqhyVKN=9CZ-{Ohi>e=~bm4IKbhM%%W zW8oXE!rGpV7Wt(_^4nndH1_imheaWzDi|I})9ZVZ9>pN+P%dVc5wG`Ze*4`@rjn1^ z`ln(;vPBHQUb}y8S>=8q__r7g+=z$>!pReVB0@XKchAvyGjLQs-u>+w%`frV4FeIG zj=7n~hGrwx*&5aHy(7X$bDZ7YhcP%(*>G^lAYMK;qG~V8Jz@b7oNg;IA1z$9@TbzW z;@I51@Ekef#qbxnG$Y8Z%bm~ibZ=4#%yKr%#b)CDrfKN`ujIY?tA4h9)i~dZ4E;ZM znvb$n2)zn$Wx&zlW%mJZDh28ox$@%`w3i7YFepXUChw}$UXKI=-TM51`M#FH=tdr*mQ!c=aB1296Lu>iTTKZWss0f z5~ihdImPN$aTle_AdbYC^31}_^EK|9R&l#%3hbx;8vJ+Gp^tm{9JDILu*1PW!rh^Dn9p<)h#Sl4kKM%nm<+!ESSk* zC;lLNT$fgr-!+{aBsSx$41b}yy6o>r3F#1&iv3cfY2N<+`0qJ+>=&Qxs}JOEkD?^l-F5i`t5+zNuvJf z3Fh4$mNqiFXL-aq4U4K@Ae$fq-TDT`rvrx;gqx96w^*@s=mcthCaIyPe(w)6kI{EqV10tcShHU9eeAPs)s?6#vrq}>y3FeTJu$Udha+z zs7}rmA@yR(L&>35sNjQqrw}o^)UitMU!5g6nnG)(tgst!^`FKJEzI1(d@j_w@;^hr zgYxlIRYjho4U$bhczfq&YySCqCE(5_d>l(4tk1v9!V7PB%Vx{QO=G2NC@c1%3rEzw zN<6i?h;CJX>h)kn49Sr)g#Em6km6ESP`1qc5C3ZHizN>r>V-fSS=X1nT{+Thh@kC! z(H=PlqDt7V6gOYezXUK-dretz!1?IUD6&eL2b!4=9h+HUO&DYZKMM>|YhlEEg?q?S z^XT4$2Fd|zT=x3U#L1|F;-#`to-Y6hiYkWdO=rRC)meY72pIfl`3zEGDU8($iWR^K zI$nq80aSJII<;#W5Pj>^_T&013BJ*O89Uoq z5>;Paa^E}xar^r=!pexg&OTM8wluk4R~Ru=)Hgk`Y#i_$jk{jc8hx}?(dW*X!l4vs z6_%$s#duJJFmaFc-5#>v6Yea=I~)s_pXGS>Tkz?s+WS}>Qp<9MappMLXpkXpSM~SmH6u)`Z5>o02kJs;w@KhdiZ3}29y*xr|6tMo zBHzGic+b+dTd!xOJ;p{Rguh^corJ;K?R6daayQKm+0rf7|AXg0qs!R9eS7t4{G=fs z1$=?kK1Ih=gEkI>@jgXDWHZt*C7FUEWs|u^pE3Z``^K|1KEC^sbN*4nQUfRc_AyE0 zn)?RrGjgPkzfE~_s!rDB!fDsV+*|kEX4+DyS#8%!cshn;s8svwBXSsDGX2ZRa0={* z=`p1F{zD17*Rk>Uk_cw3t5j=9-d6$}MoM~z{v{t^M!g75-+o8_XkP@CZWUQ2z!^26 zCNOu~hgrrK)y>bgqb{`Q_1^zrG4;cGarP!nb4E~(ZKWc`LVeEq;IewVneLp^ZU2+% z95PgN*M5v7Q;ZlGvM#`&u2NdHm%&gZ{bZM5wBCp&?HeZhwU87wyT_z!n4z+1?=RvXZ^72d*%+R1s1$KbAFtR|= zw;MEq=O7pMIKpFwKH6$OOszJAf<_Z<1)36cB>D>|Z6$gJL~jH`n3MMou$#Si%rDAu z4pSkJspG|^CJ86vg6kkfXsA_`8@8iOryOe!Qhn8SV6}mPlof3=WJRVqAr_b;e->`Z zMR(p|K|$L0^6;u~USxg#B6-ZNc%E1dv*^P=|2k*^NOBni#G%9Y?##{=)8KZwh85OL zSBG9|gb|hdmY^gn(ziY&O5#@I?W)W;361Yb^VQNpz0A7&^(7HRAsUvw#)fvhocvja zLxV65J0_$>&cVRctJFsn^qLos^tG`+B0_gQ{NeOwKt-!C^gGFufdtPT*Vi>l#X1|V z2XxsAcixN)Ekq=a##_^=k_^BFH5_zpvPDRP>u6+3$}i&b zy0@FdzAHw?i9OqnlTts_w5D@Nd#eM)KKEuN#m{|AJyscxa}(eA?z4&4yvXo{OBS65 z-?gW;<+;+ntM}U_yTmHm6*2zj0Imj<&ZgE9Wj|gfsXhrVH-c0p$7HXnR8bxDYOi z=_r3FA~u`L&2;Vir8}P3)k|@c?sK1U@&iWo{HEXcoy>6wQSuJ+b4l%aTBuigs&k@Y<2c=S3Ef?p zH>ki4yDuXdo_eu>X1{E$g(Q-u#zVXN^&%70guoizo7x(kQ0OZ}H$O9UB}(FaX8Ct1 zFpx~}EbHf2r6V;x=@8GH$C2|6*?K~?LrtMYd^bw*WYXhA z_))@RMH;nZedW3+qfWbv<|_#BYOxX^rhbN+!za)|!|8K*LRs(R$O*2SDM{g9k7e{u zN4VIdi}e#0&h?sBxu$>Yy%)j(k1V2fuhp8r!}gfF@b;F?U`6}YnnMh1&sSU&lR^?# zu!61+lGsuFEfDraX3+$QZibCbKzc{75G^T7@WZSQ)j5898G1AOXB*H*TSd`f<`IK# zm1%&t?i|2Z-a&r!pJehzg@!awNp)R)aa?q_SqGrxE5u+T#f?K2;GAHV?O&>!W@Q*k)7=g2vDW+7K zbyY9i{|nOF*SbMYoRQSAbSH2y$bE5(@d6xKxcF#@TE~X#3o=;`0sc!RupdRmQsML? z&>SCwS{FOpSr+@6Uuz3m`hj}(^g`Jz|6?({!%WVJn$H|ugxW+x-GEA?J&U^ugj3Nb z;65~)W<}iH2PJ@st8LtLfSOLXYgj=9<;?ih7rq$bXW9J#!B8!Wu6#U`A$wlcoC*&` z_9Js~7%m79#+edeT&P`@_Ng@e&5J+pqpx%31tAF71)pcz~-yJ>P5yX(nuM4;bUHDa8E(~~l{j~JeCGkX>nHJDpgSf&bTHEf)qw8{Q~CBPEVen|MW2P3vmf`8X9-g|>>ddp zcgfjbl~(?3Wa*NzQH>4nsM$3}Ul>pX1xC0oF3TZXe7=V!9!n?WgvH|R zpbruczmB%z=zkZ>=1R|gXwGThLELqD5KCUhtiRGT*JwKIvzbzV%ZU!e!VcNHSSX3> zObH|oohc8nvQZ2}q??C}@>!fe3gH+HF@4(qWqi>;ag~md#D;cl8&gQb^?2a@5cikT z=7r78@&5gV3Ggc9f=<<8v~yz`NcEGvbX1V_`IL(&+Z>LB zM~$ok2qXzod@1$TEl*U~H$V5g$er{Uj^($sWb7Nr{gsIbE(`$LRGECTOraXiU%=uq z0zvpi1S%)RxTjzoVcR4#10)fs()4Mtsa@e?9j)Bk!LsYyXIZga2q7d%`vQE!V@<1Y zmkpH3LeXJNO9f7l>F84g;huc=4nk(UnU}RLZmYk2TtB#lv34K(?8~gyx-mN%g=U44 zOPdr_!j-;IEbe|l9-buuKEy^Q9MLjSKG$S6dz)!U_32{1)N}L)3+COmlg=nY1@od$ zJ<0z-B%sisAR1yh>z-RfQQb6M4i-d#vxvb~f69M{JLPZv1JSCh1$gQ*LxOF-tH9!k zbQ0ZW)S7)qCSF|=2`q_A3}OHBNBueZwTTz^ar~gz#2KA74&&D)KHt~m4F_nK<^*7_ z!!pN@xiGkq%>1N(rNxw$zu-=1t*IpAy$ z4~dD0w%9;E?(greVWZ3(o9ux`elM>Rek#0 zO=#-(4p5B+wFzlEU7^k{3EdL6sIp|K*>xrriI`}E8ze|z-$YpN`^_teL_7P`%e>IN z7tNiH619P+0Q1hBR|W#POOta)1|LkIRtgz zMJ9VOxXN#o)mlXS=u%`Q>~PBuKEmOWsIuQRp{y%!ty{fEyL0gV)$LQeL#pqX3L@SR zJ2Gb^E9+KVd?;joVOXlGie3?z6>(>u(i!(qGz(W( ze~^xj&IRF<98ypEis{Y_FoHn%C0bW(XeF#Lj=2WUEBqKNPPFppEH?_a3}-h906X}C zSYKcZFU`Om5YlWhh@ogzCn3NvuM~F9jOX|xe-X*!YL+#ceh_tJoHXz`aTnvSrOAZ| zOtdGz?QdT!oAJr3(XL2G(p%2X4{xEohU&vd_zQ(U%ihHOlKPWnb$&YYhx48?|R++>`5?sxvM?!;ru|9 zZ#nwuTK^S%ce<+ggdJBE&fRrXN7O!{nu`%q`M{2Ef_+IRad2cf01P9pST9AOK>y75c!9}~)Et^6$`&Nm{wzWcm4c0j9DF!xJTpGrMp3esI4D_iiDe`sswXSu{dQZE_`^A11 z?Z@Hw=65mVu^%X`>;$mciK}XiZ{xw7I_!t)S00^JuxdCXhIRO~S*lPS(S^je`DH4E zxbKNs8RL`N?gCQ@YSOU=>0FE#Ku#DRO7JA&fu-X8b;3!^#{=7`WsDXUxfUsE(FKSQ z&=N`A7IwLq%+vt(F;z+T=uZNl=@K4|E%p{p^o5(BGjsE|WOR`%8+XgGW8xJTFJc4L zVY#L`OdnSM{HyS$fX1)3_JuNNH1aDsDqi>CzCT5=kY5zV<~29bX)c^I8R5n&ymHkx zj(QC4t#mDK;2xi8O%V;C{HqDQeM64=b4@sa*N_K0a&ro4+8LY6cFHz< ze|!g}zF|tDrP=`+U7KwKl20gdW1%!iN>1=uxA|NZJ2peruBOj?RBPb~8G;s6xIi6- z?_odhafsxoxiBf zwZZ)c*)FLc0#wE~bXw0TPBYl+h9hs|DYr_B4LR_YL@S1hQs=p zNEh%_fUvWZCbJtaF#kP5=(O#{8|g&Kmz1&8{@Lufw^DhtvKx955~aqxi2C=)Z-!Kd z+m-u+#^U4(HYn6a1w652kO0bYBt&goyx(n?MR^kI+{Q?0Y{G~W2) z0dS3fuJ?SU(6ZDp=kUley%PK}K_;YQyK|U|?7t9SHiyIfpT4a_kUVIhH4PSaj@3mo z`z}|mHhx1Pq?@(3vTBb5HTXuFAzFZEt0D-fw_kd=XvwIUh3VXTm{wbDA~cESd5cI1 zd>6=&AvG3yu+)`9oxmfrDQ(1fzv(_0l?bp{a364dXLRRBI8kBv!KsL;brY)#E3`o{ z3TlWUsS0{Voci?6MejccG9x_KiqN>So*1{25r6BSl9jUyR}1TgXBLL7Pr6Wv~Nu47;fbiU7TbL}>qmtl36YSZ() zVf@nqW(As~#`@bIC+AxSw!O5Pocf&rYaCFm?Jd?XR)p#@{!|5^Ws@wd855)mI^8y{ zws+VvGXW6%xoj@JkGb=~%oJ~7m6+uhOv?bH+jJJ~eFgp+}~*^C+3>R-MY!IZQoabCh( zN(T+z@Oyc^C)WqQESmh{d!!T8zS(!wX=R#hEKxMXy(eg zZ+Cwm1a%?;RH$h2_ws|nRjn8ZY!>3gn+6Ep4xT|AeFox7!rac2Lw?jsz}JqPE?5JG zok0}q1P;cuzs%Yrze|&d$oTr<`Lx{fbq2OV=!3v-ODq(n?|WxuhtmwJBIoW^^FB+D z-?Ok9HBKc5@)L(W&vmI{prL?4^OE9TR)bELS=<>*w%&aKjzi*@;5#P3moG@dm{Eke zhE#Is;&=o|{2GWai}7LYEI+gmc^Kj4K7w7n)+9godg?yB2?xs}pF1<*!Sv?D~Uvbkgs9xx9s#6zBv9l@ox>d#H6eqw^KZO;Vg}h!q zI33^$4}yF*q+q{DsJsa(SsV!YQ#zi^IF9MQV6i{SiN4dWWCi%YQ+hNc1r!^+<(YnB zG62-D`M3w3Q2;@X{S`n`{QO>migDpz0FK`->sYDOESs6u>-~<}_XN_6><2g7U#XC{ z$#Ig;n{_yEMnlvx-lP*;ts#DHV0r8j518>~33?Ak#jocW>uk>6V||p7{4rov#RS9c zdPD6r`qF1om9r!zS4Jk1>7fn#GCnmD=JIt1Na`X)=*LP7R!3XATgk`;&U*P<(0d z9p<0T&eYqQ9jot39FxpfuPSPYlfQ$s-*;+c1KL+cHIVcG5`H~^Ryu1Hk7%Nf$TCwR!SzG31@NHpm`mcp8v!wyWM49TjTxASJ-8JP*MTHLC}hF==PUOh8kaaXeGFGd<|e29vSDaS ztPeu&zv0^wN}Hahi`$pcDs~FVt2F;K!q}q*Y@{7i#stWfU`u2La4aerBKhV`^zG~j zJWvtZpcHIP7x*tfLSQcng6D(`HVp4=LWp_0Xt=2wEHjK)!DSz_Z?5J@>awRyk?azj zU-kdSs~cp))*pfJ_q7u`IsCq8F|OShB~D56S(Mwwlt?{yURE7#eI&WcpVq(@9Fd~g zeUiD!a4w51Nj(YzLnau+O3MDub|?loF0=<#jLztAM>PruE7yNDD0L}y=Ayuc?^?Ni zf~%GK=iEhn2}xKp7GonJx!JpDmDsco$|$XtRdUDwbM9$9s7x9-of2nKNj~?b@UOKz z9{`=Irz^ba-c&1vSQxSh;I2`cKc8-4)aCy%#bam;3_8vSJ-jw`_}lyukEC~z00EbC zI*dU3F21A)dSZr{qA5QF+{a%D`h#?8o%M?)*hWxuqnQD(TpcmfNq&UN$BmB)0!r8) zxno@Q?$_D&*4(rW6b+?-Y^5|*P`DHmJ%pI<6*yP)o}2^?>d7P#bd2j=vvx2mfLW@R zQLD`%buR*}nzNYNf%68w-D$7%v|=bXg1mYrdZy~}(@RRZ-U+Gx=nmCjVxr5Ag# zLw3R29-MHJl|`mRxj#sv@EfyR#-q>BE-XFEENbV$#dWM?!VjU8~kKZsd@G=HPrI{HiqN&j<92*-3$^M*;n@rG*i! zvi#?j;lc5w>@+r!6*CVUrN9as=S3?(ZBT979$5R#ZpPm?2VjIyQcEFp9orGR>f;G? zK<~FiYY6ow-&}|v7k?+03TC++so$)2~rN``u z>N%j$AbNQLX_!evzG8abf=15260vIXdz7K^a$YS)iw{@x5<|Rr#ii|ov=LJ{eu>dZYe_ip$ZuzvRu1dpjQK1BvP zH~m#t=2_wy>9+YkdNF-z` zQ*#7=^r%R*pIi2AI`>n9>(QJVE1k8?Ilav<)NUjW^O$}^yZZ{_Uwn!4Fq1`aslX;Y zj`XDIm`E1sz|wShA=?a@ZGKDSMU#Z3$E!1nZ)g^Eg3ZDoSN6@RXrGVCHvMIauS7d> zuJltXf9)LdTWdF!n%-iA9b#2$W#i??K)zYho^((ZqluvhAr@{H{diy0%@-~VW zKYC|2Ma)2^=skdLT@ZVqJfiCDqS@~qIGexL(BKy6Aw9ch0hoHN&E+m3*uka9+AIh3gTWdSe~W({-&^oFw`!j7$DcsF$7`pO?kRMK<9h=SV?cmyJIe`$4|zoI(6u9#qY9zM?#zNe^!Dl2>Z^dH`>`wSY# ztU;V*+g0R0DH6EnJA$U{QL&T~&s{`smeC2I-5mzv=v$l@iF;yN0hMibU=CG^e>J;+9k`Si9PzLaj$>}QKI6lWmO_o+_( zmhxA*0|-Na`+*J1qEMIXZf9rb#;pcOw>EDeDjb!|GumQ2!1ac;YqU|X;F@l1_lemzTN0J|U zFJF(kO21aHg)*KfuKT=BA{VDkOvlx(b{f|A9D69_BHUm#S$F>~`Mt@GesjLp3;reY zP~q>6Tt;`XkjqV?i7lqPbWGh`y<7dq<}pDHl-dDA4QG6`QDq)+vq_&HfW!}P6Cp4d zt>Qnli5ri*I1ILEOGD~3Y!@2^Jmcy1xDXmKolC?at}_6;neEfca0rLHT}NLpoUYh` zDbCtfZnYN&>}m-(F{5d1=)bBuZ?OcP`GmsQV@kn%JMJUIep`Avon#8=ATpEo-@hg& z12f-)R=HCD%pUjvbWa|P!}u)=wInpZG*LHKrZDMeC>Qils^IyY)x;kDRs4c3!DDOG zAptSsf#1X>kSli|Qka@S)6O4un-2aKL?bcV;$*>KSxHovjrfZ^-+c#>;(42yj71K| zzRyFiLrwv$rPcNA{mtv=o(*JDA0kS93>OE0D{KMJzLk$cc_5dCLWnJcFJd6_>BpE< z?aW9;^!;arQcIjloW&YL+~MkNO&a>N=pmhg>{SM<@`a&VeUA`ay*P@R$_+WS2%r?_ zs&Z%c`>ie+%!I=Lz>$9$7a`-`hoc&*dl60^whsaQ;~9~@JYn1Oc_bmgVVyAzUOYgZ z#j{`#D_YZ)(wa5;qzR#zo4a|-ANJjBB90r4Iun3*BkMxw_Ti>SjhktsmR|BPCLt>9 zZ_3eQjweI*-8+HNt)$9^s|+10w@sU!PY{`#BnF!ULS=#{k0Zr5`yOS?p8PfWbKT`6 z@T+PeRJ4`fj5t8bMs)0>o9|C>mBTlfQ*nFG#Rri-Q7}E}+eaz`LmO!`Y_pHkoAruu z`&!5VNnA3IG$}Pz)V&pt&AF!$E{J-;or3vWv3&Sl&9KzG+ae73Zf}=aP*SCI1{?0T z9SAC)W(?DSKOkcmW$(K5Bl?c@(5#>J#j@eq#ctX~$TIjkl>Wrfv%Ey+bl1Z-v?NxJ zwZ9!ae-MsHPUx&_W22?9$mCE%&~lzVG?hDXM%~gXGk+Q!Jf0BspkMWxy;^!n<6JIrSYjv z6F%~$8)0^qbUho9Sdf97b_n({$;|XH9-RHrohHuPcro@03KEPFejN&q?&nJFoIQY; zSI#uL6>2^^yOR!51OLO65xGas55dPG;3=uQ35ZYW04#+~byXQf^7Vq`G z zKpxF`G*X(YOz2^@7i#D+s-~A1E;3&x%%qL5hkiy^JhYjJ74{hvVmAx*6BH`M`!qGC zO9pjEsR)A-n1`6KLACSL%FS_Kcm+?4*z-V?WAZPs?RkzoijIr~I+oh1^~T`q^dCFvG$Gbd8AnTYBjLKYUmayaQz#S1le7Q^Hyr#;X&h*1wDpm+gZC!rSKom zq|+o&UGpeXtlQ1;?@JukKG!8PGS1Io0z6O}ZeL&DsON^I0K+>Mxv#ohK+;ByAZ`Eb z2orY{j0Pa3edA(#-pJA0AaJ6h& z81Gl(pd#j~mrizktoid14K5ig7u8FvZmLLP%l@dl05IprCyqDB?mA2fc*6UB+49lb zZ8`V9epdo=OeZoiY%zw-w`8DNwTORV_>>3T{r)1-YsGSo0E2s>tix9OBqKFBjg#}G z`pgkCblKMYs!Z)r^(qT_c+}gLhR|gnq!1~Qr|~kt&2@_yswx{i$KEn`8J1W8BGljl zr@GEG#W(s#AKKyuqLp+cl1C}7%`m#-!$15XF{M(M*-fD%+i#mFbP35jlgN3{8#A-dmj&OQtG)!031jTwGMal=&YtPfq2AUWekP9J-JT(p099!L`+yen$ zVH1?kRrhV7(mGKkm_jPP_U@Xd;x=ppk}4WY0Rbr> z0MJM_;$GGxL*P68y%KBqHntF{>X&<{aeI4m6+{TQ%~Zp}v%Pujr)zg5mV;cFKqeA- zQm5`#Sd{B6Rc*4PS-rO(vf>YEdXmOK?>K@`L5}|9q}#t_IE%g+U<-1qw3mr5&v;2A zCQ}BEn9_u;;>n5N#dP0RhCF-_UplC+U(i~Zjh>U5+b8%@p3HK(R*IMQwE!uritb}< zF)AK2?+0@-aE3LYkg`B*&N&m~JWB9>(Z>`aqRwgioU)0w{U1K4?>-#i|ZfhNa9hV)2)(%ch zJMH1twoeZWwkE@I!dz$ma+;9GeACv>Ncupl@+gBSeU_uzfj!$+h&@EACkZG_vwLGA z(?^;rcJu1$5H~xI@6lHIYC-$+b&hF1p`AoAOKqw{t0Fu#X`OGt$)7Q!nmJ=&)xjq@ zHoxT4pcYKSPT5(4yzIuQ^S*N2NJpR4v0?rB-^JuaXNLis?E(l>Jo8mUw(gsFLLOy? zEszHWGaCn|lw$LSwoj{G7Uq(zK0W^VVWu#ms8BMRlF2z%-g`fOXmndgC(na8fc)s` zz$GAoxP+l|+T_S4$r1sLwkV77ew1Gug*`|HiE*?FGLm1q; z^p0A0eqqbmk3?|!CB9DBN1Zof6d7+ zJSn!`VD~tVaqy<*Mw^8dM5v3Bvj2VdVFb=)U3L2eDM3@>n(P z?Rr_=I17+r4fE{>1LBQG0&o97nef67n-aNnVP<{dd6*B!Q344 zZbsAof&jw+;CLeK2d87t9s~YZ5?6Qwf&{NPEBN+)LbjOcZRXNcR&h)x`TtdpI+b!>$E~h0o1L*2OddpR9!Gw~-E^Cj(7i69S<66ak$)AYMv|xG+;uR(`;h zGIV3}?+Qxdjz)s;s}jHY{JPmeo@-tN$H@hxaV@)}K?y~ts~E6H(F|SlsN5oH8g7*h zGiC!8c1doE3U|D}Vul1yPmXuCk*hmyU4MG2ml#V0+(G5I+`L_=3cD$%$I=@*8m-LU-!fn&-sZO1%ls63+w}AiAK`Jv z>`q~ztr&&(gCkFpci+*1Ekdv*MhBCzGfPBj9dM|YEjZk(tWBuz4?MGeq+*)t>Q=z6UXF_w z{QDUT4^JQ8J%hW;d2xGB>Fl4Y-bRT!ttP2GE5jYoI1e(eVK0&V5W+>zludt=nf|UN zi1IV;MK$Fy%$yw<oGeW?JIGjmfGLH$Y;l|T0p1V!N*Jvu zHSAG0WpwPip0vm7%VRq8$2O2>P5b!WBfTz*6dZ4Wd6O9Y(8A;nOuG((y?F`ac_u2( z#~17CoTK)1G<~~Z4jXlout{e&nZbDHyHf(=a?OtaJ(2Q(!g#)Ugw-QQ?A?mN#yN%T zBtJ`sA6Lpg`k>Pi8a7GssiY$eG0Be8LCoQL{GDqi-;j0pLmT!Z)szldvbN7GVcu*S zzb1rEq|M)1qa7rM*I8!<#w7FnQ?{v^? z0`MlS3+`#ZB5$DT4+`7e-Hlp_2G0`*F@STbRJ|!tk3cC~1T%NR-p4s=sTT+RqsMjF zyrp-Jv?CD4Y3N&Zb1gr=%`MFR8;|r)uxQ6*X{OpEhQ~+tu}^n8Wijiy`pSMw0uKNi zSNX^Z1y;WirM0o_x%zft0U2GcLm_2BS`b{Z>g|9VOVr%QF*R?pTpiJsEbj4jLVAyd zTA;x15=f~b0^(e*Vo;Tn;WTJSxpI9LmL($Lxob<^S!k7mGhnnVNnAC*g!$ms0#Q|q zs=25I0<>fUw_&+KU`}5P9wlmjRWdMYh%Np6n?AAHQ;JzG?s(Z9UR`pNh79Nzk~DF+ zX~jy>>f-2bl?drlM8 z3NfIQnrT@pLmv+QA6efWPv!sqe;mh3_RcOj5>Ya;4hhN13dtx*_TJ-=kX_kZQDkPz zIw}#e_dK%au@1*L&iUP^cfH?zf1iK)tHv=t|>-9mMT!;;Vg|svSzWkN7q#t$c4N$Q;tl3EYwef_4q>GO<#I89VhY;`X*hz$n*GZ%f+;uViG z?uLlxD1OIeid}0r9%Ssoc7@vJjZIsZlU9zvYpjhYiOrzD5sq3OC zpf-X;Nb!DLpxqX^zDIK%=46-Z3%i-bac`RIBS5*wcw5Pu>G|kF>TQP$dGRYh#1hwD z{|cbbTOKL>Gb1-;X6?vWLC+KJ_^Ij?KzJ7eZ?^8XNgoYU9^z&>d zsIjX*uOK`#Wu!`>L@y!=XpQcW+mBaRjm|XrB@etLdr}Ob57e7EkE;7a*t7=M#XFL6 za;KHHk-rBNTjp-gS^;ehKNv>K>+_jPQ45J%4><1HyKJ?;T9#~k_23?xD}B&@Wp{%H z($hU+nWR?g!9dsJkgVz(J_Yrdns+m~9V_gQ7Sb`&F4wZZ!k}##j$>O{4{?avCbCZfyW zO$)m7LE=P?$CXHDU_RUD+sYwT;nKI7 zSs_XTv!BuxpJ!7(b~uYfsgzt~mj5(vf2r~`LHwpePs!o2A3zEr@#sxo8HEe8>V||d zBiz0@e&6}p*}!6jsm}I0bN9Mc2(c#jg@;Nu6!Kv&4&P8-UcQ-00WJIO%4OuUn;^jU z;I3r=T3KQtiMQ7&x32eVtB`mCe)9ws^7u%2P`B%Xc}=Qc&O^{FmS^{~Rho}^s`B+H z=1_T);9LRK?{$Vx22!5m)Er8aoPOA8&{7fyt`t@~Vw%gtx~+g3qs8LFR%(2Uny28A6dFYnNQgcUa>Sq=%alFh&8#@1o_qgwve* zVFimnUtL{4aHP6s?FB%bu2SP=e*VGqXC8iuZ-JOc{5%Lx0g|VvyWkdh&FD^Gkc!0N zhoolXvp6GC8wj?Y+V;r*EN+<1ac`-+!8Mqb@Nz)=OqV?4gxhR^t7*+^+AfxxVt(n{ z+fkk|-xSGqmkZa@Q%`;;r`-Z|? z0fR6b@l%pTwK*@xY+(MwBUwf^z+F*~piC64BWTrz}-HS1-XF-IA%?Zs_#F8 zcmUuEZ6Of>YIJOe$&{V;3vIBw7|jSGPeS6cvTMdj96Y~pI-z7InGW;(DhFqaiTTO9@KWvQi9__j0btLZ9 zAa~-Po%^sDFfme4@Yiq}r`BgnYK2eTwCjg9_zC4V{{&_GTm-!qHGVR6JXDjw;}GzF z6lXA{xo1+tQM{9vwb1&sRXPdGDHbEMbnwh}t+%tvcw5p4J4r#hEpDl=A{;Mjc%0)T zsG}v<$^HhdcE)5IJ^iBWK{7?Zn)vb%c!5eIj4 zbT}CGO*u)Od@^LuIC@_2{=AP2-O99NglFudj{!T}0e8wtTQcB@F9QW6$J!0Ye`T+U zXDx84b$!hD#4YzSyZLy~!IIZuFa3%eU zG4eg5?}sZ6Yj29P^-PcXG*8%VzLL$0!oL?c(!oQ+G!kORsa+lsf5YER>PX83R4LgF zgPNQJ#Bo#)MXU%J9k?RWD;c>|as5b5p>xAwau=X5XbERX`_ZHB8_XSNDe`s?n(e>) zGF$G%n6o+W{6A-@4hsIK0*J%jpB#Y*G^B48eQD(CDZR5oBl-P=)r7fH^PLf?!aK6V zwkIM35?l*I6p@;^H}JIDNs-fF*IFN?k?kj(M)QKM%%?dSkf1d$Nly2z(>)oq8z}0H zH?Qa{x&36#W@y04!9zx@x7un@ob$&)V8#f~0n1|jF0kFs4aZ{ND1~QjWHToIY5)LY zrgKDCj@dFCx&-w$QMi=CqD*=`$NqC~2k366pPXl#>Y7A=iQD}f`)+B-pS@LIW_M?9 zlBS_)(vGz!L$#P`?<3Hvonw@B1uJ244y)M?0)z0-hq++sJ0GZ+{oiiH;lFi&wy(C! z0Bv9z^M;`4@)USP)7dhg@K5K&U&|7&-@I0Sk>I+ZH75_xEn>qh9qmc%aA@NEKBsVBgUuK zC=b{w-0oU|)~tAVI zyJ3BAB}%rsjz7qZ?x_XCWe6!_u-{e_3u68Asso0IvwKdxq1lN#%4w>J zi>}P;$JZ>58(ZAjsmSJl6BWUTe`0eGEf3f_yS#H6vx;UJWO7CCK!{)4C}`C$j5gNj|k znb$4QRurEE3tPEe!JzG-a0DmvXePO zSD#Q-qOAjTMm|=aBSnvwHoEbgyVIz@J$hT*legak-hhb}e#%cm2$nR2 zV9A{kc)WT$np=5coPQIskbGMO@Fn2NxPv$@SJZdG6}jV;+%(cH+*RFQ(+DjsJlman zy`D(yN?8MCtjWD3w}Q|jQccb$}BDW%M$zZZnri2+5ls)@@(wQD`jt_GpTKL_^CO&SSCcHbfMX#JXYFI^*947 zPh&S-G=l*C@`E5CU1$m7ao(Q&oSmY7)ZZ#5_fEyYzLsFJwJ%GfErFeRN@7lUbUrL| z$6;gQSNsI91LJvT+$Zb0>g<4g8T{B!U05lfKmoSRH^pB^^8sJ3{8PzVq0NeypMF5k zU3qOqksdq{>AUjm3O~dZx^vS6C$ldgCWszl?xd8-sJ;-kPnISB*-f=L*8XggOx$?u zg%B-QovSjBbj}%sShZv~r?`*6PiiQW;nee<-=+y4}S#}q_BgXIJoSOf$YbE7vXt4;Np zrKzZf6Ny0aES8(-cqmnIGMg&ieYWryBZ0VTB=4<*@auP4NdIk&q(Mt(OLPm|Yl za!0OpC9sA#tk>OsaCSx0;!$5r6naw ztzLBo>#LKaxxsO=yWe%yGilL`A|6E#TK! z+1VRQlo*D?(k0-mlRM+`OMT8kVB*-%ZGv}Aj1u^j!wu*~>L<-T+u?6sX!3C}lQte- zk(6_=iwXsQ0JbRvJDwMnk!c99w~s~uD_4vMB=m~-ft-*|z~$*g4g;pgG~Ap1m@@Fx zWS)8IKSN6`^vVQ8hv^Oc+O(Rt7!U%wVsGP+Y6fyS%GG+v+dIdVfCXPzAV~~li+3m5 ztFQmbE)(#2#Oi@k$1#zUS6ijD_yYsa{+BHZAw+^zAEI3bc(h0qm?|pNf?oS}Km#OG zrOfCKn_-CVO;}DXu|5YE#d8I2o>}vUxYlv&>=+I28WY>a1;uI)HUM_IvpF;Ln4ROT zf!=1rpKihNFUo=R@sD-pT!EOm%%ncl43f;aem^;|A#s3`b6vjeAzO!M-gwc`-Kj~{ zBX)tq64*kJl#TrgW4o%hTY3x$P01nD6a6s2#MmwM$vyX5PU|YngU*wXGK*?f?#Eg$~^OWW3I@of-=XVuu-b%A1Z|nqY_2 z;~jD&=QnB#WGU>;RwFq(I< z34K1fCMwf9F}G%k(&?~2EY&)W*-_z0ReS$;7+I1)zz`)M zpAF{5ZHLPMJhYU z;GE*@hM1NM{G{L94dL$!Y-h6A9K9W=I6AYb`Y=v{(tpyLQz^^Aibea(q()R*TU|-m zozpyr!|-BZ_Dn+$*2|vq2Y@ghHo!-`WjVtU-bab(SJp2*2i-}$UP9^qnF_OIFS~-< zYj^VS!)Wu}vn6!LDIt!HJ1SU-@ce>z8f4cT4R9V@O^Xg9)4`VpjsXm*~@%l^Ux;Rf#Zck`BNXu0Y(!C zj%Z}UAmD00nsOS%Uull)dU(fZgJ$bo>3Oa`8h~Wt)EM?v(ndlTS1p0|E9Pg>=&>58 zghD~%R;YpqZAw;F;M(lx5b_wkVbnd+ER+6A-SYj^1XUgNGn0I~ES|f|5emjyPIW)S z0z8i6)BZt&h(qQxih4HbFYa6~jyeKbc_`QEdLD@9SBGButjw|b^l*oQjDk<7Nig08IK zb`ATVGzK%LP+>9aFM0hr8t+m`uNr?h&8o3Rp$T&ql||K}7GgobFhCViaDH~+F#yC- zt>7T3&_PZ*feTKTyd6vlF~JmEA1f+*>CCE4ex}5N^$4o)YuxX&3T$P0(IS!+kan^J z_p>v#1J8bWELml|S02YAQe-&yVew+kipZr~H-I@yc$=8#rZ-8L<_nDx&Qv3dJDwUX z!)@=h1`~R2M{$J8bM^1O&Gy2oxe1T;K?NA{iv_eYuhpLyc3%xu%z`dVc}Z}%cHGHQ<7P!Q|e?dwnSpL!AUf!B^!?#^Q#W!Ry+7ofwPZ1mZq z(Id0{htmX1W?2cAYWZo_lOtT#+Us-nlP$=CGK|Ri4x0Xh>(|iN9y1 z=9y26A4Y}ViRi9Fxzm{>J`YM>GX1D|$4BY9xJrY{oY2~Z&};B{Zq9Pp!pox`8e#0C z-h~@fohA74(#ws!{7kIe4v6XUX<)9bd)g66Bz%^Y4p0~OF+rY;l$v&7T<3~4y!bv> zR$r#LblZcVgy2lq!ff+>yuR4qCcljQa03x|dTcG7`CHcxh#POtGKt6ymNd_0qF7Wf zBj_KC8{jl!zZ>0neDp19n3sD?HC=|WM3!}cK4zCnu6Uoj*hbV1<#F2BD)@A~y%@VXx+u}Hcn=_s-({PxzmMZ^xJ1SV zoZMY*FarYvO_@z8Lr2ep)%HgIL7rhYa~#X&&V8oYSw zA4m{3{hw1Vb~~26K^xro&e7i9eg^SqK0i}kG3z(!_~E?sjJlSWIWXJqKiHAWTG*SpPcCMD`kEc1gx`R^YkYWz zEN4vEIkj@&e4tC!(_~x`-K$w6CU%X7U2Y z)Y}T5stEyoSsB{H{+xfST3tov~6@lO}2gx#N(rHXiOAHT!dp6FiV8V)B4{L_P_% zmX0rPa^-{1xG6|#uEGo+!v)QAOjRe|jg2ICcXU!|Cr+LMbLHlhJ)ErR*P9*z$NLlt zmYjAUbljq004ZyOco?HJovV7M*Wb2nF8vT2D;3kGi%F)6Kr#TVW>}zTHnUQxoGmD0CY9J`|d%8@}n;_co2q zWr98`R_c@PQbMi}x3bWo4XZj{it6qYj+o*XvNoS4>rF;7WNn;vA*|A!3H}Wh-uk@n z*hV0S+XnX;K;BOoz?&*9_{NnM25s4^^QUt|>R!()^Z6#G3OmL{CU^-IG_M7_a~B+& zCrV;ouC1ljbK(K=ygqAE_-}ewnH2&&t0enS7}I4i0wJgNvCf|P$`|DHku`K`HfDa2=n@DCg8MRi_)vpMR2Mxy4PE2Qe! zD||kNXy=0WeU(43v%md9Hg9Zu#CP%d%C67gk_#pfXs8lf>M=betm(}0fdDKq0{26# z_c?J!Cgo-~*=wswLXkR|W8d+rDdV00`22Ouv=_Hod9bmB!=D$I4r@7DZX7e+0tO!9 zR{0d}A6^K#yRx@ykotO4(WUJsmFvN)d-o-wZ(wcDSUS`8jO-JSAMa4y@MK4fDP`(P zzxQ2})ofiauWKj9{Rm$Yw^?g=?`oO(Vf|T^I+-A+o1#F`>tn59d=FtgVJAV=y;G&` z0GMvtEeil5;e$Ln8-41(UeMl2kYLk%vPl?0+Egg_;g)494o5FsvdeZKP;&&fjw7o{ z|B+e%Z|)8Ts?=>@p|hr!nYXgV=ZjI4Cp#$E>+g^6r7Nd3<>-t=G%B5IyZUI{e{49G zqnIXEB=M@5Ndf1J#l5YWcLG=A4ufF8S{z5Kz-uM?Ni{{%mr);=l0=473h#cIc{K3> zZ-VUw_Ng5^HgWQhs5tQU@qv-YBej9`R$a^|lknX<*+sSVXue8M0#EPBJ6_Liwl*8l z_zoD#!l%WIXJZ$jm?|zUu0LdeP&8IW*(|39&QzKGnem$6--u{ZGtHt#Hro*h)?lu zXGKo-4Hv1WP*VLj;uA6UwGSV*6ro%PRbwR{@tXoCOb=OFTB4ru-|Id!rP5Y6LF*-D zy|t0qDSVPo$ffyoj#CIZV?l3VsPRYye$F^xxv~Z78_fwlCWbwW!nYCR2nx0_+@tg3C_UDMVa2Br=X3hfP}^Cp4Yg=#OK}K zKYVY`V9jEKD!UrCbSX6Xym2T-cg}!n;?;o{mM|zWj0P@D|FO-rQ zKt#ApEh#AX%_f%9!G6`I*K=bSnMIhQ%W5&BOMntzVr*eS;WR;FgM)+k`#+Vze*z&V zkU^I-R|!Nwy<~>eeQ~hJqa2|DdpX15kD=6U73Du;T|VarycBP^n#IZeIJ&H3S9#@oec~poZELqX$DAc>XZyuIqd^GK0Jq~0kI=d zA7gMo8%zmkEdnqMh)tkp?V0I;Tm3`>aU3^~dXw zlhdd3=iygnUgYu#GRhxln}4D?Gokczq?T;RjCk0=fUHy18$lt!-q!%sNxee7No^+N$9d?Es*``)0UJ4SC&FNY0pf z_MlbGdUy$|F}YDvJ9GTCkZbsNKj3DL5;=BGBx8xI;n)=A0d0j6MP7Mi6MQdk@Tux2Qy`oI_&*%EQ0bE?|R>P$rDhcFa8O?JIK zPOpFDa?-L*+Q7RrCg#y5z$l0d>n@+OYo3g>-Z*x&`Jj5|=*UOYaJer6;FAbdtt0O? zrFGUE?!XeUG}G8wMgeTs%+r;3uUU;Nq5EuU{h-g&UOBKhdS`;J=m!~xn*ztv_p@dD zR)tR!P=~5kX)FRsx9)uyuu?0dh%Ht7`PTM@e#Cq!z2ts;O;L)tQ1ipDiWqbGz@o_p z^D=UKR#`S7HAt4vQtD(_SeWyj_av~#tJKlb9>-s5Ykuzx_E1ZNl4)~f=zG$*;-y=T z2ozmFva9az<{2&63fQ?(Q8{IPx@t1LuFcxP-LXVctWh3AwazVTt2)w^*Zn-#eB`bD zSHoAusjOBK5(>uQPGj=ijdOH3jqG?(<5#C{*JQ?Lt~@zow=Ii4Al$Vr!#+Cf-gx)A z`_h(>b@7?*6bYM8%628gGW^rwWoG$mK_eCk`}B&llStfwHf12*{5spmTeNH$4{gCY z@Yuwr*k@%m;T<60bw9z6^WpWi@Bu^qe-g;YAzI+VjgsuZaGA=^G*I{KLy@rIjSpWb zFQNsCp2T;S$VaJtZ<(waRu8y7^X;>YhsWp zM)mKgCeE@K;J4vQSV z&-(Gl5AJCp>K*2-`U|4i;u3p8xo6(isu-38>cY zml1Eo&FBBKJpour?}q&nggpFiGM%m+YX`ng8P+uRnJiMyWcv*_AZ8KAB$w;rfmN8C z<-2EB6TqZO>A~P{*<);wYqZgxQS8E*syOXvGkGxF@s(scud0uv?T)fQ z(DGrwM7lvpitUG~6!*}kZUpBn9PuP`5^nMK@($xI^0Q~axP5qU>L~uF{R_<9&m z({}$$WuD1y-QzMVb3jLPk`~bDJNkw(Dv-6cKUb4uzD= z-w?i0NZ2K}AbT}Zi^uOZ32xmSxJw+6(3j%a!~Tdy-@RxVx6YUw2|V6JX+mSJNclfl zF~SD#eo+lnB=ZpHLl{)E+`sI^-V1Vn!6#Ml_W4aH*Pe(++sNI`M=5L3?X1z0;CJeE zJiX5Mp6JH*=R9W0t(1@>>1y=lP^F=yJil6JxU~I}EpTsBx?rJ5LbCbQ zuLBmmX1MO&!E}khx=+#hCesIB53`IWwqyFtR{AUv7vJ{Q^dn1S0@*^UOmRwctFy&> zd={(J@avBzmu$MbyamRMt_$kfHY<*v)%%&nY4hUDH=$k)$8LHlUG0G3Kv#T~-vQjw z)hXbsNIg?~b-jRw)ir5Q(gfwM+Zk+0haf z+4ER%>T8RnKAoJ-(s&tu&-iZ@A?^J|d z6md=9C4am*v2r=aa&a?~37bc($n#wQ<8UGXL+!RtrRXGSj-2INJ#+3J=}e6nOC}G8 zN~lvCS@rxoq7w$CLg-wx!%V%ymw>~xhUw4cADX*$A}D~{21F$!Y61aHwpdL!QcrsN zl~$s5kk%7HWHkZ43%mOcwlk3RcbKGQ*}K(Fxput)rpE0zH0vY(EyY=blQZ`odG#hD z)~{&r6XkSE(^csqsaMm>2c%xsT2&g_Nab1bTY%fIoNHatDY@C@Ei~v@19|F?szU6SWRS)uDXqNY!48RlAb;S*ijqus; zp;bteR835>3BXML2CewOM<^q3M*ubU`}gnI-oS&(vf=GF|JJB-inGOH_dc1xb|iqR zWgrcNy?1*8)vAlAaiBE%K3Q>5Ygy-#Wf$>FqL|Kvgb&6H?iQC*Z|PN)xZJhH#d#=a z@s9O0oea6Lg}submzNZ{iZ*_okZ$6G*h5YO!dE=7c4=YA9g$y%1xjkVl#|1DShEjM zH3(sS?uRfB3mhW5Wrm} zrY>KpBxM&CC;s5Ie_{o}upN{vdb8x<_$5iiQN49`z`+Zz`&E`yLAim;X&}$HAfKmT zkO2Dgdno95mWMH~h2c4);H=MigT8hyzl|4g;dU7F;p^X>w!fa0zf{^rf?>~ z0w{=F_R}ru{g5i@&xwC%R-!-1x|(k6pSb5_)$f`zyErIvSCs{z`iVvU4x_znFKti!!av6BkRX_=+kEc;*`_rla zB`g4ruCJGT3XVTTrlh3Yj>1>PNIy?sV%Yo*=qaBIOY87_?P04yx6TV?_{~K? zOHEo3|2EA2JAMPYZM!H<{|!s-$r>l5{19icxV`Wf-{<0I>{v&H4FZaCy$B6Ludz{v zRH!!HV#JGP?5(L!Zp#}NlOODgWqjO+yo~+LasPYxH+ht2KjdfCFQr(oovP3?vkFK^5FvPJ4^LD=DpYQi4tUXuY1;erJaBQ79 zHcp(>mKvoD+)bq5SX9siR>(%CL??*D>Snn%p}NfGO4(RY^puLI+j$Pw)NZLb5bKo{s|0L~ z-A3R~;QHMg0bHSgESOM&N&@oF4|8gkPF-nVM=sQ;d}wcS{{!iW-)yQ``D6t#xlh(O zRF0Z@O>0uMz9g)u{P))ptV5lH2(gC8I5i(FDRG5Gp1bgBydKgxJy5gBfK(#D7NzZU zatG}S^z#KL*Do5=K*F7hk(`mbdgI1XoM!8*-};#UzNtEG@Nki#`7)GfV;VlfW^)=` zBaAjK5>gx@wf_D!B!2C6xBK^K4%x|+#?P@5N7tlfWo6xWJD~Wz^cnPfFF($Ixt4!j z9%x^1$on56XZB0Irm^kw-*rd1YVO;(*LbB21@7OPJspo%WO676#~oUMws(zP#+shG+$ns0IC3W z_{kYU>N5<_6=j>*0d}r-?8U+--eXfy2M+opoYL|=I932TMp=&k#tzJ^72OtRJ8BVOvTYPh;@EE=LJLeOk`y?d|Dd9%fWlhON^LnB^6x0LyZqz@imyogJ`$C@Lr9Z4o)ZQz>NCavG$$@e2#r3 z4I=}I5KgV>wl)~_Ja7gLQGju0c1{h%cV&6c`doWWv$>q*=ZLc8J{hBiKXNK?zx2Nr zz!pph;BLU2OaZTv>Pzj(VpSp2&OWNCF<~>NgL!nezhxEgj;&2 zl>z@V#>sykFCnFL?|(j)J3SFr|FFa`n@KbhC2pZB7 z#3>qIn&~mG_Vki=p8_x&CFeD4V7MvgJlk^G7H;(apFxr+7Gc0+1KfI6$@aeF+d7DJ~_-A|H=0?Da#&^Cqb=!=fVz>giW5nw=jWQBS%L^t1EZ@ zCm9;qlG{($@0W3T&l17ownc5pWhfM8Mwn-fLtb7H|IYl)8@QikEc_Le+s60x?&B*m z5kObB5{BD}gGr7l84~vP{N)C~3V;xhBWd%=^j0&KBw3T3-HU`;hqWA3OWW~<8nl-M zfYn-BI0_?g`3$_;&Exw<(G{QM|8)Kq28x9NF-F$>r@_BO)t^T*i-U1bX01<)zC_uE zR@8qEQQ#cm$YbXIUPVO?z7KI$pw@r=-V{V@>dC9Hn==1QBVy_b;#*jR+&f*$AwCl?o&G?2Uk4=*Ej zFK^Yvw*HTO9n!XRBWe++o3)4O!OC9PC=_l_<$M(W8(Akk`zv5?nJifb^rH3N?Hhio zo$=nNmSEz_QFHj|XF!vQEcdqPyZz_4|M_GBH)k)KA9XGRlTJD;3*y1c#?ZWkeaQM* z^`Bf04#Z)ARgrE4rMmlk8E5F=NpaW8xKNd3)-orW$m+kh(W12jQbQ7oi z)=#qbmhkplt}u`FC0sV9sdnb5$E!zX_xlA{4wW&j0*DCm`=1;Sh_sB1xiH@C89Z93;8d)EUk=lPNIZ`o3H`Vd+Ig`=CV}#?PAXvzWk{x96fn z0(rYh<>?PJ>Hd8v@c8=*vm+)>P1k@i2>yMaKw2nihLV6Z;wcdc*E2{8=xNh(FkEe3 zq_pc;ISw&}`?lqKx<4vIa67!xu|P}G$c3MDyg?u^InS?uM6Zzys0QM9ChW>g-ypzA zkOUSfvhTTWq{_>TJ{+kpgwX{@>P5ptiJ1NTO5)8 z8BiLUY_!*AJ$V386^TicK@z0qOPWP#Ea5?}!$_&fQ zOcRKuR^tLX*&CM(ahYftiNg!a=uU|He)2nU2(~iX@Yo|foZp906;o=d%aK09YEW7_ z-yX*;XE#z@?zZ&fQ?2fYX!T8@-$(K5Jo+AkyOM+(944x4B%2NR&avFFJY^9_br5UtzSX5@gmYYm@ z@S$jtqFn18bXQr0IYhQ=+2~ZDB_DRW3d=*B+3q`-*1P$i!GVIG(AMp=vBQ#^_mNxp z(;4Iz#_~&9jZ}}7oW?R;_x8&h?b0N326NJq4~>W^TeI^!o4=G5G{|9ff|`NN5+?ns zL@IWva(*@PXPmVGQ#rgIOY*nnoqNDDy$hd2uMT>wBgzg>YT&BV2U{k1ah1(1j_v0` z@o;6~SUGW=!+j!oa9ko_2^G75?VolPmWk=Pb-h{k=phZga( z88Rp7QzbHkpYG!aug9e^DF63Bi|1#CeAW^CpakO9DTT!p$yhuT8Aq10^cl2O@Zl-2RXr`+zCPj#_FqXs}W2{Qvn2Y{BmNsG45? zB{BF_rVgT$u0 zE8o6|@C>uOK1Ba}!V zx!M$9J1B7#_JSs90cKlucib?T&HqQpLE9YV1?v{gh2NWKEt9FX8;3DePnCL5Z=k)Flp=?-i$<5H4zc z`?2ZZ+p~Y8FYr;m3Vn2(u5Z`Av6#S}zkpQpZ|vNP0DY^I-oa$HXzg+ajQC7%wldRN zfOAL!UwFtuphqqR41v|3He4cQF5;UU9M~lti-k<HSTs^#>-Tf|C2&~#m%6WZAy1jz!Q_-IbpZP z8ht8}UG13lz+N-7+01+RlE)6OT^3px7fn@1|_b7^{bhPet}< z_)77(<^>8-qQ2X(n4faVhm@T0@Z{5HFSWs~EDXtV@7IAMbVUP6;v8^%l3PZ#wOZ-* z*Vk4lRj6OYpAZ_$*`t|tYKmLar&&{5{d+5cst)rQTn`n8>Xi+0zXc6YbTPMgzewFg z23F=+`8=FXXF6b*CDVN$v3|6iy;TSFSYh$qrbhKDcT^U9l zj}3g#zty{k*>s8S+>t|cng#3@Rz`z}njy{*?90mV6_Mkvv=iL9pb0ttHf$7;TxkX1 z-klTGb`2~-Mxx6~+{b-KiFd3XG`p?+6-0PMorB#Q@TY_CH5)En#5WrmHqj;@Fvi1A zeGpO@wuYIPOgRY&02e-U+j7!$LZ#5mS72R3MJS^gfheL5`kQV_n{8}KXaj)V%4b~As zFrQ7yZal}~{ELX@8c#V?2LlM@)g(|;VvcBjEuTJ=`WkOem{DL!+7Lr!U;F!mGm_^~ z+V^T?%bz+8noq9{ybcq16Gzd^fS2`skac)@6|;8X8l6Q19epZ@l^3@1ES!x2XLNA4 z_FI8#x5sq7hXVr83D;_5$sU!*Ye}zyx1wMC?Q{DSgrUx#fM?_Fj@{syA2x2yL^J{S zPPLkQ#O+9E9a^H*USdriL6rGHDt$B!vu~t7^)@_e=(<|SVd!MenX48AP(Z$4WoC9_ zeN;I;hEAr{ZvB^gK*1AWfI~5H0a{Y#2UBjn9`7;3JDrI5leeufemoZol*pDlVTSHP z3#8@6kxsJwUFg9(;)>Xm!{nsFC<7}Xwv_?o=eP)$>vvvj>yw z=YS7{pIOg(u@mJ%G0G^TM@L6>l)?_{_e`(yLxmX%h*D zMJS13@e!}HFR{?GNtq;%=4#zUgfFP^$g|Ax1<`vC&qIPbwGNo}3>ZM?=Evk6r|J&S zi$UD-za)A$kcqu)8)1mG z{FI*zS4{wM6S3;RP-!$0&8!6*;>|%T%HJxZt}cmap#~4vD0Pkx22gBbPo~=2iEMFa zSN<~qRz>jf54?e)>3%j;Gc6C1_YO0C|CDQDt7+bE({$0($tizZ)xn2L?@6_ zR3$`yiwH?E%X*^k*^oQ=z!1GA|E&fXHPR=rIEGq4%0=SGvror2Y%k#d`aPmx5@~7a zdkmPa1d-<`6M%& zp9rn|?C(5SRowEcasXoE$)s`=GvJk9wPt|2VX31T2F}6x3#(&IMqZND*a1muBh9?X zX_HSLo?$y$a;qFx^U1W|YAd%)Gaf|AEHqZ*{PW96FF*&nO-@c?c6t5=K_z@2f$8<^ zY}d|9NRviy7sF$61>@bV$B3*VeDg4DX3qScxVTL~5Go^T?}aG+th- z2`EduJx~ZcSssR;yX%oW&ze|$TF?;>HGHp~Eq?$w&SAD?d#s$$|4F@l*T7}X$7>}7 zRvPwxrPaLO5X-qYiQ7{P^4Ui2GDbq&DJ3Yu`)8zfMi1{>HEq`+uR1bJ4x!#n0D6_M8Zs_# z3mc%u30aK|avL-!XI&?{^%v4OXUr4OzaL*|-HV&M5GPx)SUqYMWw@Ex;%DHx^&FOD zncjYHD@AiYbGx1O(rsKW>Eg}cid)6bqA}!r!G{?x#)c?^k+q_uv%Xh3ha^A^{%wnpRPY({1LqK{NQy>!UjUc8f7x2` zgyLiGpsKlFO75ee2#drn3Glyna)PvUP}e(t6P z(8^W6g23+fzT5gZQQ^L-Yg#^P;QK8FTZAe)*|CKS6(I>8a2aoN+XEkYf2jAF!Zi3! zjS($tF@bu(ypeC>`IZtF;jz`F6A-Y7ZUQBuZxp&q4zHb9cc*!1`T3p9xL9`nWhNVr z!2lf=fCA>;1E&E|yfmrHqB#XnUCu28b*4#eZ{lLL(42#`ui?BO&uZj|d_Fh!Bw8g$ zn@2uezsJz@^XM(T{!CEw+EyG*eaF`FuTN%C zOZg)khBpDobCl(3ud$bhr>EdmuQ^l^Cic|y2m>LM+gsZGYKUAeJE5YUX9}j^JDoojv<}Cm&t+agmp?JE0%d#fo}m_cYogpjn5&egilTvDFz-Df}1i zB4)bXfn$dqb!cCa13DdCgMNehaa&${n5Mw&bxeKfNmHq%e{T_H@WB!H3QgFK2gNpB zP<;xkez-y-Lr(0^P^G!YH~WLut`0=mPXbVN64iv6Nd`s=eUQ;?V((+QU0&B4SF3*{Pm$AVrq;v&)c>VLy_UCe45VEsI@ZWM2TaB# zRU6XaLx0^H=0)Z!$rIu`3*s{Z!W7pU@6aHvX*vUuzME+!B5H}k_gFD)3=f;nI zi1|B!@iO%p;L{!JSEI~vyUByf_{HY=;RuAK##-h!06XFwxYi?xl}oWStJ*P{OcVe~ z_v(y8!+BaLQB`(D(XrL0ReKMn$R)8mU2@$q$Pq; zbZq-$IkP4V(`m}e<)cwnZLrjiA-X0@VY~Gi5-PKX20#Eag!JOw1br%7Rr}`(v@d!u zCo@&wE1SwM=zt~$K!eJ**9GAv!}Cogn9(d0X~BwPkU4gaWh?WVRcE3N?C%_R_D)Vw z(YmJTJ_0~fhItqHPqoIFGQYE2!~?aSRa{vjcDWhy5>oT zGOMFTWfL`aLx-!QL(9r?~D6y9Uhq=af8z!rqg#p zXk%gE-;=@G>MUv7p@P#ni@zP*$YQwA0Dlc21`%pV;p!_F@xI(^eA5&SZ{rU?^Wj}! z6Y%C^eMYilc_~MAwqV`h=I0;WA)MqJ^$IvyJ-O0)*RuLYjTL1TWd|(NbhIZ;nOop( z`4bc=fsxaeI@zc!vvYFFetFRKSMjef2_#oIzzPIxZ4oB0sxKOzX4Wltz#G@LD2Qr5 zm9o~xF;EU*_!O`}IigC{sU%1^$$B@>Fa_H0*>*1Amc^7tnKxcPpr8zZTme`6(0@J| zXfBE;0)lcuv%tqq05V8P2B^)Nhq~qdR|1KCfe>(GeuFaNc)T~zvma>o)FZv;sVD@D zynx%jpd8m<{zI zz44BQcmN85TNhy2plu`Nt$b;sKELSBpW)my@*ZnL{lFaD|7-8c-;zw*wh@(1yH+~o zQd6mwOU~P(B4CS|mX=v+F44&NRvMbQpcpDmU!|BhndzGgrsa}~;RGs*v>~aLX|A9$ zxrCyC3y6ZiciVh3@BH@t1LJY%FM8{e94DY4JQ} zYS0fcOC|N!{@iq*a@H$Qe9ONriBWJrhLhC?o5K2)!=~i)0hGh-mMd~RkqdIGCB(fU zy5*IvHssJ&gxudt>g(3w2{)axskJ_#h96qTc~<{c!`n^f zg+SOfdm8=UI!4%}d%RkXd}yWU1H66h)eDTsQr!qkcZE^zbI#F$k(dn7l7z}@YSv1+ zIcEYw{HJjfg()x7R@zQ&o;LdJ2vi6Fkl?OHM-Ga!%w}co(6=I5LZ>n{9pr~6!z|S$ zq_VfE7##n|{H(t$wPI-D`~L#((@V(MZ>p6Eb8k%4{lIGT;hZ9cg%~HhcbDCd%0RbM zs?uZG1wSL{Z0f+NzDiO?w9~XT^dWptKJ@M~0(@5*az*ZgabU465JN9eFY7vD8Wdz_ zlAIonnlivB;uDXov3sIgoKx2>G6a;@?v0qg;r`RnZ{4wMw2%}(e*c8k`R7sNT@>H} zfUU~mHR~8!4rJTHVlT=v3wz2kx&95Nz?@Tj8)s5E}t{|AFA=d_Y zOTqb{ATx>U``k~NJ2hYk3r#Gn1}|1Xj}jq!9%;{k(?9!WZt1z#{OATvapC-}#$LWi zi2R>~v0v6A<|?Eg)Ye#VyRyr7RJ$N4vFEFfmb1jHF(yZN^rc!ULDen>KWu(D9Z5!P ze(qg(G2HmSqyi2B&W`vo@N=3l?+dXbWn-`1LrY1^_mSilpKLLxQp}@s?=Tqw6Do5Pui*IhPZtaT|GAE&MF$;(4s9Bt5f+vbITElRv3( ze&@3GgY%ltiz;PZXq||TeA+sP9bc(#*G<2ck&zF3W?0$Bxit`EwvZb7jke;810>h3 zb}}!oS_xUbJ^$_PWrSlJ-;v4qq!@|L9uM#ALcMu|+|fni+AqPpu+CtjBrs#Y1jKVU zEc6L$d!2l-MgMi5&7?{Dfxj)qn;mIZudn7I6V$88%05A!PtCQTGSxXKMGh;qXa|fE zJBUmhM!}@e#A?s%bajm+=Ka1WxHZWaj;k#XT{T#;bH9c5zA8txVHEz(EeE*PP9eD9 z<2|evdxmVLj_n@`lp>6@ zy_ZTczm54_lGjPwPaq$dF1HdIks&Mp;%bge$QZnnp${}#&Z3)z95ei@b9;c=kJpY- z$G#RZbgyTi3&d4=3%+gXOSp|g^~^%K1id>re4gTka;7m@WA}bFo`GUbT8-n19VVdO}IkuW(H_iil_S}@$xy(Q*fCcNaD60 zxqsWK5lESLWnKgy^ci@da#k9^aW5)oLzbFxlUVBA&UM~79PF7=rW@Ot`>9(Gju3N{A4%EK0dPuz{=J_LUv|Pe^*x3eq_ExMNjB3?{$+xH^_Y z;e5pH)*~Lo@y=;b=P$Iqp9KR|j(>D-kaI4WeI&&HPFRtbZBMiQ^PwE`pF$Z7#(@UF zP2~&InXDTNx3`4)H2mD8yHl{Jk(|C(VA2vwY}3IRqo*qy9HvN7a!$$hlZqjmb6tZy zp1fLd^be5LmcI`_d3@@A`jLDS!b0qXVvP%y>+DfL86Ie=*TZ)PL??Lk^F};4=dwv; zPRBV>*)f&NE0vtjYHw@vs9l(Dk*g-}ARSciwv!f)E361d_9y<;9b7)PBw$3dh`AZi zAY4)BVh3t>;gR=s)nZW3PT_3bOLDK)eTZT^*m%P!HdC!FvK=Z=_iA>Bg!`SsC|P3u zz+oMr^PUcTebccFK>bqp475+?5RUC{Y7klp^p=Q;ZM+c8Zq6wBtH*5c=QHlp7wZS%6AszeebN>>_2^H7uuK@g%1{vF}DT>U{h`}c+u5ubXcFMH)fZ6-l z!y=qVN>jqgj)3T!mALcM;1!8}PDcMCU6<9?l#euNff${zE=b0d%;TcPFfw`y>zjLg#_WgnwatH|t}Y&WrR32m5W_AWNa`OqIc{ zW{_mX(Ck1psRCgMhJ*hXhcAG1ocb_kuY)%9rlYzq8h$K;X}=5m+8CYpJ4Yw6zLi%S zpu}dkAc_hVv>NfWy9eLsQ-6OzoBl{WAkRi|U;anmJ5dFwz(C9~-A(!Vfw z(E!S5ua;@}(q5GrIc6|PAOSPg{il$s$UBI}tk5xuP-VedGyZd}xqXvWvU_`{;Cf0> z5fN79T(#iq-q$RLb(of0ZA0lfepj^!a2-6 zv{v^7r2J*xmj&XVgZ>Wd=RqwGGe1`-Svll~bz(-y7*N1ooU5J*aY@&5ea5ss6n(a? z`N9l?w~=^1g2wLDVRD5ovqLc^Z#YRDFR+QYV4emH*fzOpzer3>Pudh??f``be>dD3 z)xB}1O6bZpnt=j(m92Fxq0dz89n>B05xx10QDL-YDz&e>h_u@9+RG)Pv4{2IYNiMy z8auH}j+fW*;q%Ymtbq+KI_r4gxGUeYJ>hq~vbe!N3%NntH+Dyh7I70!cu(qE_`Vp; z07NvH4Q2s#9;mKj;>umoviK|H+#CbgGq`D+QxI*$r6&D`yf%-M^{H;6gi4*j3?c9c z8$}NK?0I4%b?c`p2;SvL3*xY`0fe_KIZqPm`M%{DCrPUt{bS|zlhbHBNlUe7zcK}E z$L2zIl+z#Z!thJW!}{G&JAC@Pg`H(}GLM_m;uV}C9Yt(vF+F0Dy7{`k zY&v=ZZf?8^qSD>~2iP#{qQK632aMplZye6Q3X>dctS@JHSz2)zJaqXvFEZlr>9$oY z^&9^4pN`1EJcEw_wi@P{zJqQX470?WZTB*5Y7F!3#xJO^z|Gw@)bFoY5#daTP5OgI zcbKI$Ok(|9g_%#If*$3ga=U0_n%|#}eWwyeW~(19Te+!xF*(rd=LU(nM15;<7Z&oA zrqIw#r7}&_qgCdvS7+!|3?8w7JNRtHQ$~8Yyw(xC+n=- z7SQBo3+)tbg2NJn^=lukNOCkiEsgt~4tCrZ{aSnrHRMk@_?1^whFrEn3mT1NSC9B&c-(JrWu@FUhSNf+(>-_%kX#@LYnzq`^M#XX}(*!_LZCY za24(5Y$WH^=;GY^#0c{Y4{_!GPvm_bd#&6ypUpfwu%|+=UEe^Q+oe$7cXnyF@O67L3%SKO#rdayD^4^vH2hG{w%vp|_*jKf4 z=jb?40UP4S+Mi~(Uz(^cvgVB+r+Rt|;wnFRYcz(i=&Q14Ok=V-tTPw4%v&;ZrxI#w z6&rvLjj#yzBr5~N*7o09CkIE=>EWwo`ceL*@Y=504RB*xY#SY{)p3Gvn9zBL_FCN0 zl^axu8p~su8HpiDNi{%5ojAv1{0?t7*mflF9&Y_x4#)X(jyLl~c+s6*I1G7{zBI;tH*_ z94)o##4$cU4ohj~e#C^E><)3E`d;ftdwTQZpDmp)9)n5^+h%BE?)8LI2A`L!zjTBL zPYE&+#0&jDFc&4Tg}VC}E@4ZGyWbiK2dvn6Mpu!cQT_^6!RG!7)fE>V>?PNFm?vc5 z>A8gcW=5Xm2#LEW_;XgMQ$=Y-#lc|zs2}}2ny_4Kb%D@Vrtu6rOmUe!ph7;;L`XHi zXcDHc;OYbIk44?|A9-=Ml{Xap)^{jb5$Kl?v`CIT`bDXV*x{h+UARtzOd}#US>a%X zOdU`5^_P@lkQxB*B<&RQB?FgJOH2-~rMnXf_{5%~s&OlUM^i30FeOM{`XOXs)3_BU zEAyNr%bz8RJ=Cvw8y=)3p z`K|i!j$l~LqQ)kabHK}7WeyB$x*({t#cQWf98qh&X{R*Y--9)~g)?XCL>&z;v9#hY zTFY?DV&1fPE&*z}6Ki`Y5#(-eVYB;OzZjPSDnN%ArA8D>wODpQT4Jt}ah556JE+G_! z_P0uQ!qDhR94VdpAqajIOl4~>oTaQ8H5yXaTZUOb%cRAkWYV?KSNlTqgSM=Wgf)JP zz=?Q5f5zPEVO!NbOCbqEwP^Ff_O_`gdm67#U{Mp^_bKcq2IoO%zcJb(M5z`cjv1Ck z+!awNRhwjj6CQqu+xC#{UWo^3+h?6ymzq3r?3JV}<|u_9x=MWAm`1AqAnOsJ*@)^4 zr|`FkZlg{Cd!#Chmhn=_ZQe;~-DTUOv>)Tbmh0{z_42vWa|vNUO% z_5KA1xNHBgw0zjUH|s5xg$b4k z@Koa#-AFizrr6h2#$k*41tm7_jp$yL4X*DZcklq!u+>9E0WnhcOFPn7Vh^ao@~tno z@RwY)*+8&|Hpdq)`a=L*Teuw;_B@u;o!a!YaOO@bs-?*gqpm?nRkXl~mKFfF z+OVzE%RlC`M5-+KM_GXZ@9b;=2C(sq+R&Ko_RzZ%5P~kDieK3yzV4BN*{$E%KY;4k z)s?*vacHYN~u+?SoI`e@S2!9Co!cdvz;@N@{yj`0-9^8osR(V7PR-O&gM)x3owqs5oJpIwc zgY`#VzjI$V>YYDrIr8D;0JK<10@ycefw z;;oV(!gUR*xBg%xTl-#d>u(5}#jFrLKo}q0b{IuuZhuO7n++ zo@9)d#`(AT$mbW5g;c;&z>1_2Nk%;L?TIhfeK%PYp>5N<5wdihxw4-qvVsN6t@bol zDFgi~t`B&ZU3ek!#fXVE5Ao$7AwI+@amT_m2SclwQE{cLcv3kwhokq+!S%>Fe_*(Z z75)vhq@YqZqa~Hf$0S?T@nr_%mV%*aT${~4)6|(P@Bq_Q!VC4tZa`7?ra`4?oV+wSr2`TVSUmKS_>V@3%0*S#!+L=3f@oF=4k9U9xv0p1;Fx&}V;X2J~h zcz^}G3|;s8JyEFR*LB*fPUm+?f+ofnBQ5uK%NrwA+RV_~h<6-mw_wU?NGRI!zNTh% z&>ty6x8&gW75gdW)?p->&%?{*brS|k@b|(>&<^nyO55Pi_q*eK)=J*Uunw2cw--p%E!VXuDa? ztZ$HPKJ6$Sh7!UrpxVBLFSnpZOw$(ftvg!Nk1LVfL+FL(u zh1Abu(oCSmgqQ2IrE;Zz2f2DAD%T4XO6tU&)2IB}vV3{^xpz1MYFEPy_09RP2QvmA zIqw<(UaCnCs!mFX$+3sjnV*(O5)y`jW!*wzF-l^K`Bxgap+0Ej z@c^nf{Ic`6I5#9bcE7fwiiP8JZ9dr3FsD~SBiW_`8{UgFt*{$@qj#E)90JYra>Zs3 z$sCTuzOye2GdTO;4@;wgJK@!ij-|c--insluCR}{#q=D6Xz#nL6;`rkc*UzLTR%Y{ zN2YK;Zcz4YY=+|(0_?E=#~3U@I1fIyRiBF zIeWj=id+b|L;kSMs>NMfeB^(={IdrC;NYJy_$L+olL`OdOqgH0OpSa?FTRhwb<|%A Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 zcmaiYXH?Tqu=Xz`p-L#B_gI#0we$cm_HcmYFP$?wjD#BaCN4mzC5#`>w9y6=ThxrYZc0WPXprg zYjB`UsV}0=eUtY$(P6YW}npdd;%9pi?zS3k-nqCob zSX_AQEf|=wYT3r?f!*Yt)ar^;l3Sro{z(7deUBPd2~(SzZ-s@0r&~Km2S?8r##9-< z)2UOSVaHqq6}%sA9Ww;V2LG=PnNAh6mA2iWOuV7T_lRDR z&N8-eN=U)-T|;wo^Wv=34wtV0g}sAAe}`Ph@~!|<;z7*K8(qkX0}o=!(+N*UWrkEja*$_H6mhK1u{P!AC39} z|3+Z(mAOq#XRYS)TLoHv<)d%$$I@+x+2)V{@o~~J-!YUI-Q9%!Ldi4Op&Lw&B>jj* zwAgC#Y>gbIqv!d|J5f!$dbCXoq(l3GR(S>(rtZ~Z*agXMMKN!@mWT_vmCbSd3dUUm z4M&+gz?@^#RRGal%G3dDvj7C5QTb@9+!MG+>0dcjtZEB45c+qx*c?)d<%htn1o!#1 zpIGonh>P1LHu3s)fGFF-qS}AXjW|M*2Xjkh7(~r(lN=o#mBD9?jt74=Rz85I4Nfx_ z7Z)q?!};>IUjMNM6ee2Thq7))a>My?iWFxQ&}WvsFP5LP+iGz+QiYek+K1`bZiTV- zHHYng?ct@Uw5!gquJ(tEv1wTrRR7cemI>aSzLI^$PxW`wL_zt@RSfZ1M3c2sbebM* ze0=;sy^!90gL~YKISz*x;*^~hcCoO&CRD)zjT(A2b_uRue=QXFe5|!cf0z1m!iwv5GUnLw9Dr*Ux z)3Lc!J@Ei;&&yxGpf2kn@2wJ2?t6~obUg;?tBiD#uo$SkFIasu+^~h33W~`r82rSa ztyE;ehFjC2hjpJ-e__EH&z?!~>UBb=&%DS>NT)1O3Isn-!SElBV2!~m6v0$vx^a<@ISutdTk1@?;i z<8w#b-%|a#?e5(n@7>M|v<<0Kpg?BiHYMRe!3Z{wYc2hN{2`6(;q`9BtXIhVq6t~KMH~J0~XtUuT06hL8c1BYZWhN zk4F2I;|za*R{ToHH2L?MfRAm5(i1Ijw;f+0&J}pZ=A0;A4M`|10ZskA!a4VibFKn^ zdVH4OlsFV{R}vFlD~aA4xxSCTTMW@Gws4bFWI@xume%smAnuJ0b91QIF?ZV!%VSRJ zO7FmG!swKO{xuH{DYZ^##gGrXsUwYfD0dxXX3>QmD&`mSi;k)YvEQX?UyfIjQeIm! z0ME3gmQ`qRZ;{qYOWt}$-mW*>D~SPZKOgP)T-Sg%d;cw^#$>3A9I(%#vsTRQe%moT zU`geRJ16l>FV^HKX1GG7fR9AT((jaVb~E|0(c-WYQscVl(z?W!rJp`etF$dBXP|EG z=WXbcZ8mI)WBN>3<@%4eD597FD5nlZajwh8(c$lum>yP)F}=(D5g1-WVZRc)(!E3} z-6jy(x$OZOwE=~{EQS(Tp`yV2&t;KBpG*XWX!yG+>tc4aoxbXi7u@O*8WWFOxUjcq z^uV_|*818$+@_{|d~VOP{NcNi+FpJ9)aA2So<7sB%j`$Prje&auIiTBb{oD7q~3g0 z>QNIwcz(V-y{Ona?L&=JaV5`o71nIsWUMA~HOdCs10H+Irew#Kr(2cn>orG2J!jvP zqcVX0OiF}c<)+5&p}a>_Uuv)L_j}nqnJ5a?RPBNi8k$R~zpZ33AA4=xJ@Z($s3pG9 zkURJY5ZI=cZGRt_;`hs$kE@B0FrRx(6K{`i1^*TY;Vn?|IAv9|NrN*KnJqO|8$e1& zb?OgMV&q5|w7PNlHLHF) zB+AK#?EtCgCvwvZ6*u|TDhJcCO+%I^@Td8CR}+nz;OZ*4Dn?mSi97m*CXXc=};!P`B?}X`F-B5v-%ACa8fo0W++j&ztmqK z;&A)cT4ob9&MxpQU41agyMU8jFq~RzXOAsy>}hBQdFVL%aTn~M>5t9go2j$i9=(rZ zADmVj;Qntcr3NIPPTggpUxL_z#5~C!Gk2Rk^3jSiDqsbpOXf^f&|h^jT4|l2ehPat zb$<*B+x^qO8Po2+DAmrQ$Zqc`1%?gp*mDk>ERf6I|42^tjR6>}4`F_Mo^N(~Spjcg z_uY$}zui*PuDJjrpP0Pd+x^5ds3TG#f?57dFL{auS_W8|G*o}gcnsKYjS6*t8VI<) zcjqTzW(Hk*t-Qhq`Xe+x%}sxXRerScbPGv8hlJ;CnU-!Nl=# zR=iTFf9`EItr9iAlAGi}i&~nJ-&+)Y| zMZigh{LXe)uR+4D_Yb+1?I93mHQ5{pId2Fq%DBr7`?ipi;CT!Q&|EO3gH~7g?8>~l zT@%*5BbetH)~%TrAF1!-!=)`FIS{^EVA4WlXYtEy^|@y@yr!C~gX+cp2;|O4x1_Ol z4fPOE^nj(}KPQasY#U{m)}TZt1C5O}vz`A|1J!-D)bR%^+=J-yJsQXDzFiqb+PT0! zIaDWWU(AfOKlSBMS};3xBN*1F2j1-_=%o($ETm8@oR_NvtMDVIv_k zlnNBiHU&h8425{MCa=`vb2YP5KM7**!{1O>5Khzu+5OVGY;V=Vl+24fOE;tMfujoF z0M``}MNnTg3f%Uy6hZi$#g%PUA_-W>uVCYpE*1j>U8cYP6m(>KAVCmbsDf39Lqv0^ zt}V6FWjOU@AbruB7MH2XqtnwiXS2scgjVMH&aF~AIduh#^aT1>*V>-st8%=Kk*{bL zzbQcK(l2~)*A8gvfX=RPsNnjfkRZ@3DZ*ff5rmx{@iYJV+a@&++}ZW+za2fU>&(4y`6wgMpQGG5Ah(9oGcJ^P(H< zvYn5JE$2B`Z7F6ihy>_49!6}(-)oZ(zryIXt=*a$bpIw^k?>RJ2 zQYr>-D#T`2ZWDU$pM89Cl+C<;J!EzHwn(NNnWpYFqDDZ_*FZ{9KQRcSrl5T>dj+eA zi|okW;6)6LR5zebZJtZ%6Gx8^=2d9>_670!8Qm$wd+?zc4RAfV!ZZ$jV0qrv(D`db zm_T*KGCh3CJGb(*X6nXzh!h9@BZ-NO8py|wG8Qv^N*g?kouH4%QkPU~Vizh-D3<@% zGomx%q42B7B}?MVdv1DFb!axQ73AUxqr!yTyFlp%Z1IAgG49usqaEbI_RnbweR;Xs zpJq7GKL_iqi8Md?f>cR?^0CA+Uk(#mTlGdZbuC*$PrdB$+EGiW**=$A3X&^lM^K2s zzwc3LtEs5|ho z2>U(-GL`}eNgL-nv3h7E<*<>C%O^=mmmX0`jQb6$mP7jUKaY4je&dCG{x$`0=_s$+ zSpgn!8f~ya&U@c%{HyrmiW2&Wzc#Sw@+14sCpTWReYpF9EQ|7vF*g|sqG3hx67g}9 zwUj5QP2Q-(KxovRtL|-62_QsHLD4Mu&qS|iDp%!rs(~ah8FcrGb?Uv^Qub5ZT_kn%I^U2rxo1DDpmN@8uejxik`DK2~IDi1d?%~pR7i#KTS zA78XRx<(RYO0_uKnw~vBKi9zX8VnjZEi?vD?YAw}y+)wIjIVg&5(=%rjx3xQ_vGCy z*&$A+bT#9%ZjI;0w(k$|*x{I1c!ECMus|TEA#QE%#&LxfGvijl7Ih!B2 z6((F_gwkV;+oSKrtr&pX&fKo3s3`TG@ye+k3Ov)<#J|p8?vKh@<$YE@YIU1~@7{f+ zydTna#zv?)6&s=1gqH<-piG>E6XW8ZI7&b@-+Yk0Oan_CW!~Q2R{QvMm8_W1IV8<+ zQTyy=(Wf*qcQubRK)$B;QF}Y>V6d_NM#=-ydM?%EPo$Q+jkf}*UrzR?Nsf?~pzIj$ z<$wN;7c!WDZ(G_7N@YgZ``l;_eAd3+;omNjlpfn;0(B7L)^;;1SsI6Le+c^ULe;O@ zl+Z@OOAr4$a;=I~R0w4jO`*PKBp?3K+uJ+Tu8^%i<_~bU!p%so z^sjol^slR`W@jiqn!M~eClIIl+`A5%lGT{z^mRbpv}~AyO%R*jmG_Wrng{B9TwIuS z0!@fsM~!57K1l0%{yy(#no}roy#r!?0wm~HT!vLDfEBs9x#`9yCKgufm0MjVRfZ=f z4*ZRc2Lgr(P+j2zQE_JzYmP0*;trl7{*N341Cq}%^M^VC3gKG-hY zmPT>ECyrhIoFhnMB^qpdbiuI}pk{qPbK^}0?Rf7^{98+95zNq6!RuV_zAe&nDk0;f zez~oXlE5%ve^TmBEt*x_X#fs(-En$jXr-R4sb$b~`nS=iOy|OVrph(U&cVS!IhmZ~ zKIRA9X%Wp1J=vTvHZ~SDe_JXOe9*fa zgEPf;gD^|qE=dl>Qkx3(80#SE7oxXQ(n4qQ#by{uppSKoDbaq`U+fRqk0BwI>IXV3 zD#K%ASkzd7u>@|pA=)Z>rQr@dLH}*r7r0ng zxa^eME+l*s7{5TNu!+bD{Pp@2)v%g6^>yj{XP&mShhg9GszNu4ITW=XCIUp2Xro&1 zg_D=J3r)6hp$8+94?D$Yn2@Kp-3LDsci)<-H!wCeQt$e9Jk)K86hvV^*Nj-Ea*o;G zsuhRw$H{$o>8qByz1V!(yV{p_0X?Kmy%g#1oSmlHsw;FQ%j9S#}ha zm0Nx09@jmOtP8Q+onN^BAgd8QI^(y!n;-APUpo5WVdmp8!`yKTlF>cqn>ag`4;o>i zl!M0G-(S*fm6VjYy}J}0nX7nJ$h`|b&KuW4d&W5IhbR;-)*9Y0(Jj|@j`$xoPQ=Cl literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-uN6Zl8~_0DOkGXc0001@sz3l12C6Xg{AT~( zm6w64BA|AX`Ve)YY-glyudNN>MAfkXz-T7`_`fEolM;0T0BA)(02-OaW z0*cW7Z~ec94o8&g0D$N>b!COu{=m}^%oXZ4?T8ZyPZuGGBPBA7pbQMoV5HYhiT?%! zcae~`(QAN4&}-=#2f5fkn!SWGWmSeCISBcS=1-U|MEoKq=k?_x3apK>9((R zuu$9X?^8?@(a{qMS%J8SJPq))v}Q-ZyDm6Gbie0m92=`YlwnQPQP1kGSm(N2UJ3P6 z^{p-u)SSCTW~c1rw;cM)-uL2{->wCn2{#%;AtCQ!m%AakVs1K#v@(*-6QavyY&v&*wO_rCJXJuq$c$7ZjsW+pJo-$L^@!7X04CvaOpPyfw|FKvu;e(&Iw>Tbg zL}#8e^?X%TReXTt>gsBByt0kSU20oQx*~P=4`&tcZ7N6t-6LiK{LxX*p6}9c<0Pu^ zLx1w_P4P2V>bX=`F%v$#{sUDdF|;rbI{p#ZW`00Bgh(eB(nOIhy8W9T>3aQ=k8Z9% zB+TusFABF~J?N~fAd}1Rme=@4+1=M{^P`~se7}e3;mY0!%#MJf!XSrUC{0uZqMAd7%q zQY#$A>q}noIB4g54Ue)x>ofVm3DKBbUmS4Z-bm7KdKsUixva)1*&z5rgAG2gxG+_x zqT-KNY4g7eM!?>==;uD9Y4iI(Hu$pl8!LrK_Zb}5nv(XKW{9R144E!cFf36p{i|8pRL~p`_^iNo z{mf7y`#hejw#^#7oKPlN_Td{psNpNnM?{7{R-ICBtYxk>?3}OTH_8WkfaTLw)ZRTfxjW+0>gMe zpKg~`Bc$Y>^VX;ks^J0oKhB#6Ukt{oQhN+o2FKGZx}~j`cQB%vVsMFnm~R_1Y&Ml? zwFfb~d|dW~UktY@?zkau>Owe zRroi(<)c4Ux&wJfY=3I=vg)uh;sL(IYY9r$WK1$F;jYqq1>xT{LCkIMb3t2jN8d`9 z=4(v-z7vHucc_fjkpS}mGC{ND+J-hc_0Ix4kT^~{-2n|;Jmn|Xf9wGudDk7bi*?^+ z7fku8z*mbkGm&xf&lmu#=b5mp{X(AwtLTf!N`7FmOmX=4xwbD=fEo8CaB1d1=$|)+ z+Dlf^GzGOdlqTO8EwO?8;r+b;gkaF^$;+#~2_YYVH!hD6r;PaWdm#V=BJ1gH9ZK_9 zrAiIC-)z)hRq6i5+$JVmR!m4P>3yJ%lH)O&wtCyum3A*})*fHODD2nq!1@M>t@Za+ zH6{(Vf>_7!I-APmpsGLYpl7jww@s5hHOj5LCQXh)YAp+y{gG(0UMm(Ur z3o3n36oFwCkn+H*GZ-c6$Y!5r3z*@z0`NrB2C^q#LkOuooUM8Oek2KBk}o1PU8&2L z4iNkb5CqJWs58aR394iCU^ImDqV;q_Pp?pl=RB2372(Io^GA^+oKguO1(x$0<7w3z z)j{vnqEB679Rz4i4t;8|&Zg77UrklxY9@GDq(ZphH6=sW`;@uIt5B?7Oi?A0-BL}(#1&R;>2aFdq+E{jsvpNHjLx2t{@g1}c~DQcPNmVmy| zNMO@ewD^+T!|!DCOf}s9dLJU}(KZy@Jc&2Nq3^;vHTs}Hgcp`cw&gd7#N}nAFe3cM1TF%vKbKSffd&~FG9y$gLyr{#to)nxz5cCASEzQ}gz8O)phtHuKOW6p z@EQF(R>j%~P63Wfosrz8p(F=D|Mff~chUGn(<=CQbSiZ{t!e zeDU-pPsLgtc#d`3PYr$i*AaT!zF#23htIG&?QfcUk+@k$LZI}v+js|yuGmE!PvAV3 ztzh90rK-0L6P}s?1QH`Ot@ilbgMBzWIs zIs6K<_NL$O4lwR%zH4oJ+}JJp-bL6~%k&p)NGDMNZX7)0kni&%^sH|T?A)`z z=adV?!qnWx^B$|LD3BaA(G=ePL1+}8iu^SnnD;VE1@VLHMVdSN9$d)R(Wk{JEOp(P zm3LtAL$b^*JsQ0W&eLaoYag~=fRRdI>#FaELCO7L>zXe6w*nxN$Iy*Q*ftHUX0+N- zU>{D_;RRVPbQ?U+$^%{lhOMKyE5>$?U1aEPist+r)b47_LehJGTu>TcgZe&J{ z{q&D{^Ps~z7|zj~rpoh2I_{gAYNoCIJmio3B}$!5vTF*h$Q*vFj~qbo%bJCCRy509 zHTdDh_HYH8Zb9`}D5;;J9fkWOQi%Y$B1!b9+ESj+B@dtAztlY2O3NE<6HFiqOF&p_ zW-K`KiY@RPSY-p9Q99}Hcd05DT79_pfb{BV7r~?9pWh=;mcKBLTen%THFPo2NN~Nf zriOtFnqx}rtO|A6k!r6 zf-z?y-UD{dT0kT9FJ`-oWuPHbo+3wBS(}?2ql(+e@VTExmfnB*liCb zmeI+v5*+W_L;&kQN^ChW{jE0Mw#0Tfs}`9bk3&7UjxP^Ke(%eJu2{VnW?tu7Iqecm zB5|=-QdzK$=h50~{X3*w4%o1FS_u(dG2s&427$lJ?6bkLet}yYXCy)u_Io1&g^c#( z-$yYmSpxz{>BL;~c+~sxJIe1$7eZI_9t`eB^Pr0)5CuA}w;;7#RvPq|H6!byRzIJG ziQ7a4y_vhj(AL`8PhIm9edCv|%TX#f50lt8+&V+D4<}IA@S@#f4xId80oH$!_!q?@ zFRGGg2mTv&@76P7aTI{)Hu%>3QS_d)pQ%g8BYi58K~m-Ov^7r8BhX7YC1D3vwz&N8{?H*_U7DI?CI)+et?q|eGu>42NJ?K4SY zD?kc>h@%4IqNYuQ8m10+8xr2HYg2qFNdJl=Tmp&ybF>1>pqVfa%SsV*BY$d6<@iJA ziyvKnZ(~F9xQNokBgMci#pnZ}Igh0@S~cYcU_2Jfuf|d3tuH?ZSSYBfM(Y3-JBsC|S9c;# zyIMkPxgrq};0T09pjj#X?W^TFCMf1-9P{)g88;NDI+S4DXe>7d3Mb~i-h&S|Jy{J< zq3736$bH?@{!amD!1Ys-X)9V=#Z={fzsjVYMX5BG6%}tkzwC#1nQLj1y1f#}8**4Y zAvDZHw8)N)8~oWC88CgzbwOrL9HFbk4}h85^ptuu7A+uc#$f^9`EWv1Vr{5+@~@Uv z#B<;-nt;)!k|fRIg;2DZ(A2M2aC65kOIov|?Mhi1Sl7YOU4c$T(DoRQIGY`ycfkn% zViHzL;E*A{`&L?GP06Foa38+QNGA zw3+Wqs(@q+H{XLJbwZzE(omw%9~LPZfYB|NF5%j%E5kr_xE0u;i?IOIchn~VjeDZ) zAqsqhP0vu2&Tbz3IgJvMpKbThC-@=nk)!|?MIPP>MggZg{cUcKsP8|N#cG5 zUXMXxcXBF9`p>09IR?x$Ry3;q@x*%}G#lnB1}r#!WL88I@uvm}X98cZ8KO&cqT1p> z+gT=IxPsq%n4GWgh-Bk8E4!~`r@t>DaQKsjDqYc&h$p~TCh8_Mck5UB84u6Jl@kUZCU9BA-S!*bf>ZotFX9?a_^y%)yH~rsAz0M5#^Di80_tgoKw(egN z`)#(MqAI&A84J#Z<|4`Co8`iY+Cv&iboMJ^f9ROUK0Lm$;-T*c;TCTED_0|qfhlcS zv;BD*$Zko#nWPL}2K8T-?4}p{u)4xon!v_(yVW8VMpxg4Kh^J6WM{IlD{s?%XRT8P|yCU`R&6gwB~ zg}{At!iWCzOH37!ytcPeC`(({ovP7M5Y@bYYMZ}P2Z3=Y_hT)4DRk}wfeIo%q*M9UvXYJq!-@Ly79m5aLD{hf@BzQB>FdQ4mw z6$@vzSKF^Gnzc9vbccii)==~9H#KW<6)Uy1wb~auBn6s`ct!ZEos`WK8e2%<00b%# zY9Nvnmj@V^K(a_38dw-S*;G-(i(ETuIwyirs?$FFW@|66a38k+a%GLmucL%Wc8qk3 z?h_4!?4Y-xt)ry)>J`SuY**fuq2>u+)VZ+_1Egzctb*xJ6+7q`K$^f~r|!i?(07CD zH!)C_uerf-AHNa?6Y61D_MjGu*|wcO+ZMOo4q2bWpvjEWK9yASk%)QhwZS%N2_F4& z16D18>e%Q1mZb`R;vW{+IUoKE`y3(7p zplg5cBB)dtf^SdLd4n60oWie|(ZjgZa6L*VKq02Aij+?Qfr#1z#fwh92aV-HGd^_w zsucG24j8b|pk>BO7k8dS86>f-jBP^Sa}SF{YNn=^NU9mLOdKcAstv&GV>r zLxKHPkFxpvE8^r@MSF6UA}cG`#yFL8;kA7ccH9D=BGBtW2;H>C`FjnF^P}(G{wU;G z!LXLCbPfsGeLCQ{Ep$^~)@?v`q(uI`CxBY44osPcq@(rR-633!qa zsyb>?v%@X+e|Mg`+kRL*(;X>^BNZz{_kw5+K;w?#pReiw7eU8_Z^hhJ&fj80XQkuU z39?-z)6Fy$I`bEiMheS(iB6uLmiMd1i)cbK*9iPpl+h4x9ch7x- z1h4H;W_G?|)i`z??KNJVwgfuAM=7&Apd3vm#AT8uzQZ!NII}}@!j)eIfn53h{NmN7 zAKG6SnKP%^k&R~m5#@_4B@V?hYyHkm>0SQ@PPiw*@Tp@UhP-?w@jW?nxXuCipMW=L zH*5l*d@+jXm0tIMP_ec6Jcy6$w(gKK@xBX8@%oPaSyG;13qkFb*LuVx3{AgIyy&n3 z@R2_DcEn|75_?-v5_o~%xEt~ONB>M~tpL!nOVBLPN&e5bn5>+7o0?Nm|EGJ5 zmUbF{u|Qn?cu5}n4@9}g(G1JxtzkKv(tqwm_?1`?YSVA2IS4WI+*(2D*wh&6MIEhw z+B+2U<&E&|YA=3>?^i6)@n1&&;WGHF-pqi_sN&^C9xoxME5UgorQ_hh1__zzR#zVC zOQt4q6>ME^iPJ37*(kg4^=EFqyKH@6HEHXy79oLj{vFqZGY?sVjk!BX^h$SFJlJnv z5uw~2jLpA)|0=tp>qG*tuLru?-u`khGG2)o{+iDx&nC}eWj3^zx|T`xn5SuR;Aw8U z`p&>dJw`F17@J8YAuW4=;leBE%qagVTG5SZdh&d)(#ZhowZ|cvWvGMMrfVsbg>_~! z19fRz8CSJdrD|Rl)w!uznBF&2-dg{>y4l+6(L(vzbLA0Bk&`=;oQQ>(M8G=3kto_) zP8HD*n4?MySO2YrG6fwSrVmnesW+D&fxjfEmp=tPd?RKLZJcH&K(-S+x)2~QZ$c(> zru?MND7_HPZJVF%wX(49H)+~!7*!I8w72v&{b={#l9yz+S_aVPc_So%iF8>$XD1q1 zFtucO=rBj0Ctmi0{njN8l@}!LX}@dwl>3yMxZ;7 z0Ff2oh8L)YuaAGOuZ5`-p%Z4H@H$;_XRJQ|&(MhO78E|nyFa158gAxG^SP(vGi^+< zChY}o(_=ci3Wta#|K6MVljNe0T$%Q5ylx-v`R)r8;3+VUpp-)7T`-Y&{Zk z*)1*2MW+_eOJtF5tCMDV`}jg-R(_IzeE9|MBKl;a7&(pCLz}5<Zf+)T7bgNUQ_!gZtMlw=8doE}#W+`Xp~1DlE=d5SPT?ymu!r4z%&#A-@x^=QfvDkfx5-jz+h zoZ1OK)2|}_+UI)i9%8sJ9X<7AA?g&_Wd7g#rttHZE;J*7!e5B^zdb%jBj&dUDg4&B zMMYrJ$Z%t!5z6=pMGuO-VF~2dwjoXY+kvR>`N7UYfIBMZGP|C7*O=tU z2Tg_xi#Q3S=1|=WRfZD;HT<1D?GMR%5kI^KWwGrC@P2@R>mDT^3qsmbBiJc21kip~ zZp<7;^w{R;JqZ)C4z-^wL=&dBYj9WJBh&rd^A^n@07qM$c+kGv^f+~mU5_*|eePF| z3wDo-qaoRjmIw<2DjMTG4$HP{z54_te_{W^gu8$r=q0JgowzgQPct2JNtWPUsjF8R zvit&V8$(;7a_m%%9TqPkCXYUp&k*MRcwr*24>hR! z$4c#E=PVE=P4MLTUBM z7#*RDe0}=B)(3cvNpOmWa*eH#2HR?NVqXdJ=hq);MGD07JIQQ7Y0#iD!$C+mk7x&B zMwkS@H%>|fmSu#+ zI!}Sb(%o29Vkp_Th>&&!k7O>Ba#Om~B_J{pT7BHHd8(Ede(l`7O#`_}19hr_?~JP9 z`q(`<)y>%)x;O7)#-wfCP{?llFMoH!)ZomgsOYFvZ1DxrlYhkWRw#E-#Qf*z@Y-EQ z1~?_=c@M4DO@8AzZ2hKvw8CgitzI9yFd&N1-{|vP#4IqYb*#S0e3hrjsEGlnc4xwk z4o!0rxpUt8j&`mJ8?+P8G{m^jbk)bo_UPM+ifW*y-A*et`#_Ja_3nYyRa9fAG1Xr5 z>#AM_@PY|*u)DGRWJihZvgEh#{*joJN28uN7;i5{kJ*Gb-TERfN{ERe_~$Es~NJCpdKLRvdj4658uYYx{ng7I<6j~w@p%F<7a(Ssib|j z51;=Py(Nu*#hnLx@w&8X%=jrADn3TW>kplnb zYbFIWWVQXN7%Cwn6KnR)kYePEBmvM45I)UJb$)ninpdYg3a5N6pm_7Q+9>!_^xy?k za8@tJ@OOs-pRAAfT>Nc2x=>sZUs2!9Dwa%TTmDggH4fq(x^MW>mcRyJINlAqK$YQCMgR8`>6=Sg$ zFnJZsA8xUBXIN3i70Q%8px@yQPMgVP=>xcPI38jNJK<=6hC={a07+n@R|$bnhB)X$ z(Zc%tadp70vBTnW{OUIjTMe38F}JIH$#A}PB&RosPyFZMD}q}5W%$rh>5#U;m`z2K zc(&WRxx7DQLM-+--^w*EWAIS%bi>h587qkwu|H=hma3T^bGD&Z!`u(RKLeNZ&pI=q$|HOcji(0P1QC!YkAp*u z3%S$kumxR}jU<@6`;*-9=5-&LYRA<~uFrwO3U0k*4|xUTp4ZY7;Zbjx|uw&BWU$zK(w55pWa~#=f$c zNDW0O68N!xCy>G}(CX=;8hJLxAKn@Aj(dbZxO8a$+L$jK8$N-h@4$i8)WqD_%Snh4 zR?{O%k}>lr>w$b$g=VP8mckcCrjnp>uQl5F_6dPM8FWRqs}h`DpfCv20uZhyY~tr8 zkAYW4#yM;*je)n=EAb(q@5BWD8b1_--m$Q-3wbh1hM{8ihq7UUQfg@)l06}y+#=$( z$x>oVYJ47zAC^>HLRE-!HitjUixP6!R98WU+h>zct7g4eD;Mj#FL*a!VW!v-@b(Jv zj@@xM5noCp5%Vk3vY{tyI#oyDV7<$`KG`tktVyC&0DqxA#>V;-3oH%NW|Q&=UQ&zU zXNIT67J4D%5R1k#bW0F}TD`hlW7b)-=-%X4;UxQ*u4bK$mTAp%y&-(?{sXF%e_VH6 zTkt(X)SSN|;8q@8XX6qfR;*$r#HbIrvOj*-5ND8RCrcw4u8D$LXm5zlj@E5<3S0R# z??=E$p{tOk96$SloZ~ARe5`J=dB|Nj?u|zy2r(-*(q^@YwZiTF@QzQyPx_l=IDKa) zqD@0?IHJqSqZ_5`)81?4^~`yiGh6>7?|dKa8!e|}5@&qV!Iu9<@G?E}Vx9EzomB3t zEbMEm$TKGwkHDpirp;FZD#6P5qIlQJ8}rf;lHoz#h4TFFPYmS3+8(13_Mx2`?^=8S z|0)0&dQLJTU6{b%*yrpQe#OKKCrL8}YKw+<#|m`SkgeoN69TzIBQOl_Yg)W*w?NW) z*WxhEp$zQBBazJSE6ygu@O^!@Fr46j=|K`Mmb~xbggw7<)BuC@cT@Bwb^k?o-A zKX^9AyqR?zBtW5UA#siILztgOp?r4qgC`9jYJG_fxlsVSugGprremg-W(K0{O!Nw-DN%=FYCyfYA3&p*K>+|Q}s4rx#CQK zNj^U;sLM#q8}#|PeC$p&jAjqMu(lkp-_50Y&n=qF9`a3`Pr9f;b`-~YZ+Bb0r~c+V z*JJ&|^T{}IHkwjNAaM^V*IQ;rk^hnnA@~?YL}7~^St}XfHf6OMMCd9!vhk#gRA*{L zp?&63axj|Si%^NW05#87zpU_>QpFNb+I00v@cHwvdBn+Un)n2Egdt~LcWOeBW4Okm zD$-e~RD+W|UB;KQ;a7GOU&%p*efGu2$@wR74+&iP8|6#_fmnh^WcJLs)rtz{46);F z4v0OL{ZP9550>2%FE(;SbM*#sqMl*UXOb>ch`fJ|(*bOZ9=EB1+V4fkQ)hjsm3-u^Pk-4ji_uDDHdD>84tER!MvbH`*tG zzvbhBR@}Yd`azQGavooV=<WbvWLlO#x`hyO34mKcxrGv=`{ssnP=0Be5#1B;Co9 zh{TR>tjW2Ny$ZxJpYeg57#0`GP#jxDCU0!H15nL@@G*HLQcRdcsUO3sO9xvtmUcc{F*>FQZcZ5bgwaS^k-j5mmt zI7Z{Xnoml|A(&_{imAjK!kf5>g(oDqDI4C{;Bv162k8sFNr;!qPa2LPh>=1n z=^_9)TsLDvTqK7&*Vfm5k;VXjBW^qN3Tl&}K=X5)oXJs$z3gk0_+7`mJvz{pK|FVs zHw!k&7xVjvY;|(Py<;J{)b#Yjj*LZO7x|~pO4^MJ2LqK3X;Irb%nf}L|gck zE#55_BNsy6m+W{e zo!P59DDo*s@VIi+S|v93PwY6d?CE=S&!JLXwE9{i)DMO*_X90;n2*mPDrL%{iqN!?%-_95J^L z=l<*{em(6|h7DR4+4G3Wr;4*}yrBkbe3}=p7sOW1xj!EZVKSMSd;QPw>uhKK z#>MlS@RB@-`ULv|#zI5GytO{=zp*R__uK~R6&p$q{Y{iNkg61yAgB8C^oy&``{~FK z8hE}H&nIihSozKrOONe5Hu?0Zy04U#0$fB7C6y~?8{or}KNvP)an=QP&W80mj&8WL zEZQF&*FhoMMG6tOjeiCIV;T{I>jhi9hiUwz?bkX3NS-k5eWKy)Mo_orMEg4sV6R6X&i-Q%JG;Esl+kLpn@Bsls9O|i9z`tKB^~1D5)RIBB&J<6T@a4$pUvh$IR$%ubH)joi z!7>ON0DPwx=>0DA>Bb^c?L8N0BBrMl#oDB+GOXJh;Y&6I)#GRy$W5xK%a;KS8BrER zX)M>Rdoc*bqP*L9DDA3lF%U8Yzb6RyIsW@}IKq^i7v&{LeIc=*ZHIbO68x=d=+0T( zev=DT9f|x!IWZNTB#N7}V4;9#V$%Wo0%g>*!MdLOEU>My0^gni9ocID{$g9ytD!gy zKRWT`DVN(lcYjR|(}f0?zgBa3SwunLfAhx><%u0uFkrdyqlh8_g zDKt#R6rA2(Vm2LW_>3lBNYKG_F{TEnnKWGGC15y&OebIRhFL4TeMR*v9i0wPoK#H< zu4){s4K&K)K(9~jgGm;H7lS7y_RYfS;&!Oj5*eqbvEcW^a*i67nevzOZxN6F+K~A%TYEtsAVsR z@J=1hc#Dgs7J2^FL|qV&#WBFQyDtEQ2kPO7m2`)WFhqAob)Y>@{crkil6w9VoA?M6 zADGq*#-hyEVhDG5MQj677XmcWY1_-UO40QEP&+D)rZoYv^1B_^w7zAvWGw&pQyCyx zD|ga$w!ODOxxGf_Qq%V9Z7Q2pFiUOIK818AGeZ-~*R zI1O|SSc=3Z?#61Rd|AXx2)K|F@Z1@x!hBBMhAqiU)J=U|Y)T$h3D?ZPPQgkSosnN! zIqw-t$0fqsOlgw3TlHJF*t$Q@bg$9}A3X=cS@-yU3_vNG_!#9}7=q7!LZ?-%U26W4 z$d>_}*s1>Ac%3uFR;tnl*fNlylJ)}r2^Q3&@+is3BIv<}x>-^_ng;jhdaM}6Sg3?p z0jS|b%QyScy3OQ(V*~l~bK>VC{9@FMuW_JUZO?y(V?LKWD6(MXzh}M3r3{7b4eB(#`(q1m{>Be%_<9jw8HO!x#yF6vez$c#kR+}s zZO-_;25Sxngd(}){zv?ccbLqRAlo;yog>4LH&uZUK1n>x?u49C)Y&2evH5Zgt~666 z_2_z|H5AO5Iqxv_Bn~*y1qzRPcob<+Otod5Xd2&z=C;u+F}zBB@b^UdGdUz|s!H}M zXG%KiLzn3G?FZgdY&3pV$nSeY?ZbU^jhLz9!t0K?ep}EFNqR1@E!f*n>x*!uO*~JF zW9UXWrVgbX1n#76_;&0S7z}(5n-bqnII}_iDsNqfmye@)kRk`w~1 z6j4h4BxcPe6}v)xGm%=z2#tB#^KwbgMTl2I*$9eY|EWAHFc3tO48Xo5rW z5oHD!G4kb?MdrOHV=A+8ThlIqL8Uu+7{G@ zb)cGBm|S^Eh5= z^E^SZ=yeC;6nNCdztw&TdnIz}^Of@Ke*@vjt)0g>Y!4AJvWiL~e7+9#Ibhe)> ziNwh>gWZL@FlWc)wzihocz+%+@*euwXhW%Hb>l7tf8aJe5_ZSH1w-uG|B;9qpcBP0 zM`r1Hu#htOl)4Cl1c7oY^t0e4Jh$-I(}M5kzWqh{F=g&IM#JiC`NDSd@BCKX#y<P@Gwl$3a3w z6<(b|K(X5FIR22M)sy$4jY*F4tT{?wZRI+KkZFb<@j@_C316lu1hq2hA|1wCmR+S@ zRN)YNNE{}i_H`_h&VUT5=Y(lN%m?%QX;6$*1P}K-PcPx>*S55v)qZ@r&Vcic-sjkm z! z=nfW&X`}iAqa_H$H%z3Tyz5&P3%+;93_0b;zxLs)t#B|up}JyV$W4~`8E@+BHQ+!y zuIo-jW!~)MN$2eHwyx-{fyGjAWJ(l8TZtUp?wZWBZ%}krT{f*^fqUh+ywHifw)_F> zp76_kj_B&zFmv$FsPm|L7%x-j!WP>_P6dHnUTv!9ZWrrmAUteBa`rT7$2ixO;ga8U z3!91micm}{!Btk+I%pMgcKs?H4`i+=w0@Ws-CS&n^=2hFTQ#QeOmSz6ttIkzmh^`A zYPq)G1l3h(E$mkyr{mvz*MP`x+PULBn%CDhltKkNo6Uqg!vJ#DA@BIYr9TQ`18Un2 zv$}BYzOQuay9}w(?JV63F$H6WmlYPPpH=R|CPb%C@BCv|&Q|&IcW7*LX?Q%epS z`=CPx{1HnJ9_46^=0VmNb>8JvMw-@&+V8SDLRYsa>hZXEeRbtf5eJ>0@Ds47zIY{N z42EOP9J8G@MXXdeiPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qcb^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZxl zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbGh)%Q!Av#fJM3Csc_g zC2I6x%$)80`Tkz#KRA!h1FzY`?0es3t!rKDT5EjPe6B=BLPr7s0GW!if;Ip^!AmGW zL;$`Vdre+|FA!I4r6)keFvAx3M#1`}ijBHDzy)3t0gwjl|qC2YB`SSxFKHr(oY#H$)x{L$LL zBdLKTlsOrmb>T0wd=&6l3+_Te>1!j0OU8%b%N342^opKmT)gni(wV($s(>V-fUv@0p8!f`=>PxC|9=nu ze{ToBBj8b<{PLfXV$h8YPgA~E!_sF9bl;QOF{o6t&JdsX?}rW!_&d`#wlB6T_h;Xf zl{4Tz5>qjF4kZgjO7ZiLPRz_~U@k5%?=30+nxEh9?s78gZ07YHB`FV`4%hlQlMJe@J`+e(qzy+h(9yY^ckv_* zb_E6o4p)ZaWfraIoB2)U7_@l(J0O%jm+Or>8}zSSTkM$ASG^w3F|I? z$+eHt7T~04(_WfKh27zqS$6* zzyy-ZyqvSIZ0!kkSvHknm_P*{5TKLQs8S6M=ONuKAUJWtpxbL#2(_huvY(v~Y%%#~ zYgsq$JbLLprKkV)32`liIT$KKEqs$iYxjFlHiRNvBhxbDg*3@Qefw4UM$>i${R5uB zhvTgmqQsKA{vrKN;TSJU2$f9q=y{$oH{<)woSeV>fkIz6D8@KB zf4M%v%f5U2?<8B(xn}xV+gWP?t&oiapJhJbfa;agtz-YM7=hrSuxl8lAc3GgFna#7 zNjX7;`d?oD`#AK+fQ=ZXqfIZFEk{ApzjJF0=yO~Yj{7oQfXl+6v!wNnoqwEvrs81a zGC?yXeSD2NV!ejp{LdZGEtd1TJ)3g{P6j#2jLR`cpo;YX}~_gU&Gd<+~SUJVh+$7S%`zLy^QqndN<_9 zrLwnXrLvW+ew9zX2)5qw7)zIYawgMrh`{_|(nx%u-ur1B7YcLp&WFa24gAuw~& zKJD3~^`Vp_SR$WGGBaMnttT)#fCc^+P$@UHIyBu+TRJWbcw4`CYL@SVGh!X&y%!x~ zaO*m-bTadEcEL6V6*{>irB8qT5Tqd54TC4`h`PVcd^AM6^Qf=GS->x%N70SY-u?qr>o2*OV7LQ=j)pQGv%4~z zz?X;qv*l$QSNjOuQZ>&WZs2^@G^Qas`T8iM{b19dS>DaXX~=jd4B2u`P;B}JjRBi# z_a@&Z5ev1-VphmKlZEZZd2-Lsw!+1S60YwW6@>+NQ=E5PZ+OUEXjgUaXL-E0fo(E* zsjQ{s>n33o#VZm0e%H{`KJi@2ghl8g>a~`?mFjw+$zlt|VJhSU@Y%0TWs>cnD&61fW4e0vFSaXZa4-c}U{4QR8U z;GV3^@(?Dk5uc@RT|+5C8-24->1snH6-?(nwXSnPcLn#X_}y3XS)MI_?zQ$ZAuyg+ z-pjqsw}|hg{$~f0FzmmbZzFC0He_*Vx|_uLc!Ffeb8#+@m#Z^AYcWcZF(^Os8&Z4g zG)y{$_pgrv#=_rV^D|Y<_b@ICleUv>c<0HzJDOsgJb#Rd-Vt@+EBDPyq7dUM9O{Yp zuGUrO?ma2wpuJuwl1M=*+tb|qx7Doj?!F-3Z>Dq_ihFP=d@_JO;vF{iu-6MWYn#=2 zRX6W=`Q`q-+q@Db|6_a1#8B|#%hskH82lS|9`im0UOJn?N#S;Y0$%xZw3*jR(1h5s z?-7D1tnIafviko>q6$UyqVDq1o@cwyCb*})l~x<@s$5D6N=-Uo1yc49p)xMzxwnuZ zHt!(hu-Ek;Fv4MyNTgbW%rPF*dB=;@r3YnrlFV{#-*gKS_qA(G-~TAlZ@Ti~Yxw;k za1EYyX_Up|`rpbZ0&Iv#$;eC|c0r4XGaQ-1mw@M_4p3vKIIpKs49a8Ns#ni)G314Z z8$Ei?AhiT5dQGWUYdCS|IC7r z=-8ol>V?u!n%F*J^^PZ(ONT&$Ph;r6X;pj|03HlDY6r~0g~X#zuzVU%a&!fs_f|m?qYvg^Z{y?9Qh7Rn?T*F%7lUtA6U&={HzhYEzA`knx1VH> z{tqv?p@I(&ObD5L4|YJV$QM>Nh-X3cx{I&!$FoPC_2iIEJfPk-$;4wz>adRu@n`_y z_R6aN|MDHdK;+IJmyw(hMoDCFCQ(6?hCAG5&7p{y->0Uckv# zvooVuu04$+pqof777ftk<#42@KQ((5DPcSMQyzGOJ{e9H$a9<2Qi_oHjl{#=FUL9d z+~0^2`tcvmp0hENwfHR`Ce|<1S@p;MNGInXCtHnrDPXCKmMTZQ{HVm_cZ>@?Wa6}O zHsJc7wE)mc@1OR2DWY%ZIPK1J2p6XDO$ar`$RXkbW}=@rFZ(t85AS>>U0!yt9f49^ zA9@pc0P#k;>+o5bJfx0t)Lq#v4`OcQn~av__dZ-RYOYu}F#pdsl31C^+Qgro}$q~5A<*c|kypzd} ziYGZ~?}5o`S5lw^B{O@laad9M_DuJle- z*9C7o=CJh#QL=V^sFlJ0c?BaB#4bV^T(DS6&Ne&DBM_3E$S^S13qC$7_Z?GYXTpR@wqr70wu$7+qvf-SEUa5mdHvFbu^7ew!Z1a^ zo}xKOuT*gtGws-a{Tx}{#(>G~Y_h&5P@Q8&p!{*s37^QX_Ibx<6XU*AtDOIvk|^{~ zPlS}&DM5$Ffyu-T&0|KS;Wnaqw{9DB&B3}vcO14wn;)O_e@2*9B&0I_ zZz{}CMxx`hv-XouY>^$Y@J(_INeM>lIQI@I>dBAqq1)}?Xmx(qRuX^i4IV%=MF306 z9g)i*79pP%_7Ex?m6ag-4Tlm=Z;?DQDyC-NpUIb#_^~V_tsL<~5<&;Gf2N+p?(msn zzUD~g>OoW@O}y0@Z;RN)wjam`CipmT&O7a|YljZqU=U86 zedayEdY)2F#BJ6xvmW8K&ffdS*0!%N<%RB!2~PAT4AD*$W7yzHbX#Eja9%3aD+Ah2 zf#T;XJW-GMxpE=d4Y>}jE=#U`IqgSoWcuvgaWQ9j1CKzG zDkoMDDT)B;Byl3R2PtC`ip=yGybfzmVNEx{xi_1|Cbqj>=FxQc{g`xj6fIfy`D8fA z##!-H_e6o0>6Su&$H2kQTujtbtyNFeKc}2=|4IfLTnye#@$Au7Kv4)dnA;-fz@D_8 z)>irG$)dkBY~zX zC!ZXLy*L3xr6cb70QqfN#Q>lFIc<>}>la4@3%7#>a1$PU&O^&VszpxLC%*!m-cO{B z-Y}rQr4$84(hvy#R69H{H zJ*O#uJh)TF6fbXy;fZkk%X=CjsTK}o5N1a`d7kgYYZLPxsHx%9*_XN8VWXEkVJZ%A z1A+5(B;0^{T4aPYr8%i@i32h)_)|q?9vws)r+=5u)1YNftF5mknwfd*%jXA2TeP}Z zQ!m?xJ3?9LpPM?_A3$hQ1QxNbR&}^m z!F999s?p^ak#C4NM_x2p9FoXWJ$>r?lJ)2bG)sX{gExgLA2s5RwHV!h6!C~d_H||J z>9{E{mEv{Z1z~65Vix@dqM4ZqiU|!)eWX$mwS5mLSufxbpBqqS!jShq1bmwCR6 z4uBri7ezMeS6ycaXPVu(i2up$L; zjpMtB`k~WaNrdgM_R=e#SN?Oa*u%nQy01?()h4A(jyfeNfx;5o+kX?maO4#1A^L}0 zYNyIh@QVXIFiS0*tE}2SWTrWNP3pH}1Vz1;E{@JbbgDFM-_Mky^7gH}LEhl~Ve5PexgbIyZ(IN%PqcaV@*_`ZFb=`EjspSz%5m2E34BVT)d=LGyHVz@-e%9Ova*{5@RD;7=Ebkc2GP%pIP^P7KzKapnh`UpH?@h z$RBpD*{b?vhohOKf-JG3?A|AX|2pQ?(>dwIbWhZ38GbTm4AImRNdv_&<99ySX;kJ| zo|5YgbHZC#HYgjBZrvGAT4NZYbp}qkVSa;C-LGsR26Co+i_HM&{awuO9l)Ml{G8zD zs$M8R`r+>PT#Rg!J(K6T4xHq7+tscU(}N$HY;Yz*cUObX7J7h0#u)S7b~t^Oj}TBF zuzsugnst;F#^1jm>22*AC$heublWtaQyM6RuaquFd8V#hJ60Z3j7@bAs&?dD#*>H0SJaDwp%U~27>zdtn+ z|8sZzklZy$%S|+^ie&P6++>zbrq&?+{Yy11Y>@_ce@vU4ZulS@6yziG6;iu3Iu`M= zf3rcWG<+3F`K|*(`0mE<$89F@jSq;j=W#E>(R}2drCB7D*0-|D;S;(;TwzIJkGs|q z2qH{m_zZ+el`b;Bv-#bQ>}*VPYC|7`rgBFf2oivXS^>v<&HHTypvd4|-zn|=h=TG{ z05TH2+{T%EnADO>3i|CB zCu60#qk`}GW{n4l-E$VrqgZGbI zbQW690KgZt4U3F^5@bdO1!xu~p@7Y~*_FfWg2CdvED5P5#w#V46LH`<&V0{t&Ml~4 zHNi7lIa+#i+^Z6EnxO7KJQw)wD)4~&S-Ki8)3=jpqxmx6c&zU&<&h%*c$I(5{1HZT zc9WE}ijcWJiVa^Q^xC|WX0habl89qycOyeViIbi(LFsEY_8a|+X^+%Qv+W4vzj>`y zpuRnjc-eHNkvXvI_f{=*FX=OKQzT?bck#2*qoKTHmDe>CDb&3AngA1O)1b}QJ1Tun z_<@yVEM>qG7664Pa@dzL@;DEh`#?yM+M|_fQS<7yv|i*pw)|Z8)9IR+QB7N3v3K(wv4OY*TXnH&X0nQB}?|h2XQeGL^q~N7N zDFa@x0E(UyN7k9g%IFq7Sf+EAfE#K%%#`)!90_)Dmy3Bll&e1vHQyPA87TaF(xbqMpDntVp?;8*$87STop$!EAnGhZ?>mqPJ(X zFsr336p3P{PpZCGn&^LP(JjnBbl_3P3Kcq+m}xVFMVr1zdCPJMDIV_ki#c=vvTwbU z*gKtfic&{<5ozL6Vfpx>o2Tts?3fkhWnJD&^$&+Mh5WGGyO7fG@6WDE`tEe(8<;+q z@Ld~g08XDzF8xtmpIj`#q^(Ty{Hq>t*v`pedHnuj(0%L(%sjkwp%s}wMd!a<*L~9T z9MM@s)Km~ogxlqEhIw5(lc46gCPsSosUFsgGDr8H{mj%OzJz{N#;bQ;KkV+ZWA1(9 zu0PXzyh+C<4OBYQ0v3z~Lr;=C@qmt8===Ov2lJ1=DeLfq*#jgT{YQCuwz?j{&3o_6 zsqp2Z_q-YWJg?C6=!Or|b@(zxTlg$ng2eUQzuC<+o)k<6^9ju_Z*#x+oioZ5T8Z_L zz9^A1h2eFS0O5muq8;LuDKwOv4A9pxmOjgb6L*i!-(0`Ie^d5Fsgspon%X|7 zC{RRXEmYn!5zP9XjG*{pLa)!2;PJB2<-tH@R7+E1cRo=Wz_5Ko8h8bB$QU%t9#vol zAoq?C$~~AsYC|AQQ)>>7BJ@{Cal)ZpqE=gjT+Juf!RD-;U0mbV1ED5PbvFD6M=qj1 zZ{QERT5@(&LQ~1X9xSf&@%r|3`S#ZCE=sWD`D4YQZ`MR`G&s>lN{y2+HqCfvgcw3E z-}Kp(dfGG?V|97kAHQX+OcKCZS`Q%}HD6u*e$~Ki&Vx53&FC!x94xJd4F2l^qQeFO z?&JdmgrdVjroKNJx64C!H&Vncr^w zzR#XI}Dn&o8jB~_YlVM^+#0W(G1LZH5K^|uYT@KSR z^Y5>^*Bc45E1({~EJB(t@4n9gb-eT#s@@7)J^^<_VV`Pm!h7av8XH6^5zO zOcQBhTGr;|MbRsgxCW69w{bl4EW#A~);L?d4*y#j8Ne=Z@fmJP0k4{_cQ~KA|Y#_#BuUiYx8y*za3_6Y}c=GSe7(2|KAfhdzud!Zq&}j)=o4 z7R|&&oX7~e@~HmyOOsCCwy`AR+deNjZ3bf6ijI_*tKP*_5JP3;0d;L_p(c>W1b%sG zJ*$wcO$ng^aW0E(5ldckV9unU7}OB7s?Wx(761?1^&8tA5y0_(ieV>(x-e@}1`lWC z-YH~G$D>#ud!SxK2_Iw{K%92=+{4yb-_XC>ji&j7)1ofp(OGa4jjF;Hd*`6YQL+Jf zffg+6CPc8F@EDPN{Kn96yip;?g@)qgkPo^nVKFqY?8!=h$G$V=<>%5J&iVjwR!7H0 z$@QL|_Q81I;Bnq8-5JyNRv$Y>`sWl{qhq>u+X|)@cMlsG!{*lu?*H`Tp|!uv z9oEPU1jUEj@ueBr}%Y)7Luyi)REaJV>eQ{+uy4uh0ep0){t;OU8D*RZ& zE-Z-&=BrWQLAD^A&qut&4{ZfhqK1ZQB0fACP)=zgx(0(o-`U62EzTkBkG@mXqbjXm z>w`HNeQM?Is&4xq@BB(K;wv5nI6EXas)XXAkUuf}5uSrZLYxRCQPefn-1^#OCd4aO zzF=dQ*CREEyWf@n6h7(uXLNgJIwGp#Xrsj6S<^bzQ7N0B0N{XlT;`=m9Olg<>KL}9 zlp>EKTx-h|%d1Ncqa=wnQEuE;sIO-f#%Bs?g4}&xS?$9MG?n$isHky0caj za8W+B^ERK#&h?(x)7LLpOqApV5F>sqB`sntV%SV>Q1;ax67qs+WcssfFeF3Xk=e4^ zjR2^(%K1oBq%0%Rf!y&WT;lu2Co(rHi|r1_uW)n{<7fGc-c=ft7Z0Q}r4W$o$@tQF#i?jDBwZ8h+=SC}3?anUp3mtRVv9l#H?-UD;HjTF zQ*>|}e=6gDrgI9p%c&4iMUkQa4zziS$bO&i#DI$Wu$7dz7-}XLk%!US^XUIFf2obO zFCTjVEtkvYSKWB;<0C;_B{HHs~ax_48^Cml*mjfBC5*7^HJZiLDir(3k&BerVIZF8zF;0q80eX8c zPN4tc+Dc5DqEAq$Y3B3R&XPZ=AQfFMXv#!RQnGecJONe0H;+!f^h5x0wS<+%;D}MpUbTNUBA}S2n&U59-_5HKr{L^jPsV8B^%NaH|tUr)mq=qCBv_- ziZ1xUp(ZzxUYTCF@C}To;u60?RIfTGS?#JnB8S8@j`TKPkAa)$My+6ziGaBcA@){d z91)%+v2_ba7gNecdj^8*I4#<11l!{XKl6s0zkXfJPxhP+@b+5ev{a>p*W-3*25c&} zmCf{g9mPWVQ$?Sp*4V|lT@~>RR)9iNdN^7KT@>*MU3&v^3e?=NTbG9!h6C|9zO097 zN{Qs6YwR-5$)~ z`b~qs`a1Dbx8P>%V=1XGjBptMf%P~sl1qbHVm1HYpY|-Z^Dar8^HqjIw}xaeRlsYa zJ_@Apy-??`gxPmb`m`0`z`#G7*_C}qiSZe~l2z65tE~IwMw$1|-u&t|z-8SxliH00 zlh1#kuqB56s+E&PWQ7Nz17?c}pN+A@-c^xLqh(j;mS|?>(Pf7(?qd z5q@jkc^nA&!K-}-1P=Ry0yyze0W!+h^iW}7jzC1{?|rEFFWbE^Yu7Y}t?jmP-D$f+ zmqFT7nTl0HL|4jwGm7w@a>9 zKD)V~+g~ysmei$OT5}%$&LK8?ib|8aY|>W3;P+0B;=oD=?1rg+PxKcP(d;OEzq1CKA&y#boc51P^ZJPPS)z5 zAZ)dd2$glGQXFj$`XBBJyl2y-aoBA8121JC9&~|_nY>nkmW>TLi%mWdn-^Jks-Jv| zSR*wij;A3Fcy8KsDjQ15?Z9oOj|Qw2;jgJiq>dxG(2I2RE- z$As!#zSFIskebqU2bnoM^N<4VWD2#>!;saPSsY8OaCCQqkCMdje$C?Sp%V}f2~tG5 z0whMYk6tcaABwu*x)ak@n4sMElGPX1_lmv@bgdI2jPdD|2-<~Jf`L`@>Lj7{<-uLQ zE3S_#3e10q-ra=vaDQ42QUY^@edh>tnTtpBiiDVUk5+Po@%RmuTntOlE29I4MeJI?;`7;{3e4Qst#i-RH6s;>e(Sc+ubF2_gwf5Qi%P!aa89fx6^{~A*&B4Q zKTF|Kx^NkiWx=RDhe<{PWXMQ;2)=SC=yZC&mh?T&CvFVz?5cW~ritRjG2?I0Av_cI z)=s!@MXpXbarYm>Kj0wOxl=eFMgSMc?62U#2gM^li@wKPK9^;;0_h7B>F>0>I3P`{ zr^ygPYp~WVm?Qbp6O3*O2)(`y)x>%ZXtztz zMAcwKDr=TCMY!S-MJ8|2MJCVNUBI0BkJV6?(!~W!_dC{TS=eh}t#X+2D>Kp&)ZN~q zvg!ogxUXu^y(P*;Q+y_rDoGeSCYxkaGPldDDx)k;ocJvvGO#1YKoQLHUf2h_pjm&1 zqh&!_KFH03FcJvSdfgUYMp=5EpigZ*8}7N_W%Ms^WSQ4hH`9>3061OEcxmf~TcYn5_oHtscWn zo5!ayj<_fZ)vHu3!A!7M;4y1QIr8YGy$P2qDD_4+T8^=^dB6uNsz|D>p~4pF3Nrb6 zcpRK*($<~JUqOya#M1=#IhOZ zG)W+rJS-x(6EoVz)P zsSo>JtnChdj9^);su%SkFG~_7JPM zEDz3gk2T7Y%x>1tWyia|op(ilEzvAujW?Xwlw>J6d7yEi8E zv30riR|a_MM%ZZX&n!qm0{2agq(s?x9E@=*tyT$nND+{Djpm7Rsy!+c$j+wqMwTOF zZL8BQ|I`<^bGW)5apO{lh(Asqen?_U`$_n0-Ob~Yd%^89oEe%9yGumQ_8Be+l2k+n zCxT%s?bMpv|AdWP7M1LQwLm|x+igA~;+iK-*+tClF&ueX_V}>=4gvZ01xpubQWXD_ zi?Un>&3=$fu)dgk-Z;0Ll}HK5_YM->l^Czrd0^cJ))(DwL2g3aZuza7ga9^|mT_70 z))}A}r1#-(9cxtn<9jGRwOB4hb9kK@YCgjfOM-90I$8@l=H^`K$cyhe2mTM|FY9vW znH~h)I<_aa#V1xmhk?Ng@$Jw-s%a!$BI4Us+Df+?J&gKAF-M`v}j`OWKP3>6`X`tEmhe#y*(Xm$_^Ybbs=%;L7h zp7q^C*qM}Krqsinq|WolR99>_!GL#Z71Hhz|IwQQv<>Ds09B?Je(lhI1(FInO8mc} zl$RyKCUmfku+Cd^8s0|t+e}5g7M{ZPJQH=UB3(~U&(w#Bz#@DTDHy>_UaS~AtN>4O zJ-I#U@R($fgupHebcpuEBX`SZ>kN!rW$#9>s{^3`86ZRQRtYTY)hiFm_9wU3c`SC8 z-5M%g)h}3Pt|wyj#F%}pGC@VL`9&>9P+_UbudCkS%y2w&*o})hBplrB*@Z?gel5q+ z%|*59(sR9GMk3xME}wd%&k?7~J)OL`rK#4d-haC7uaU8-L@?$K6(r<0e<;y83rK&` z3Q!1rD9WkcB8WBQ|WT|$u^lkr0UL4WH4EQTJyk@5gzHb18cOte4w zS`fLv8q;PvAZyY;*Go3Qw1~5#gP0D0ERla6M6#{; zr1l?bR}Nh+OC7)4bfAs(0ZD(axaw6j9v`^jh5>*Eo&$dAnt?c|Y*ckEORIiJXfGcM zEo`bmIq6rJm`XhkXR-^3d8^RTK2;nmVetHfUNugJG(4XLOu>HJA;0EWb~?&|0abr6 zxqVp@p=b3MN^|~?djPe!=eex(u!x>RYFAj|*T$cTi*Sd3Bme7Pri1tkK9N`KtRmXf zZYNBNtik97ct1R^vamQBfo9ZUR@k*LhIg8OR9d_{iv#t)LQV91^5}K5u{eyxwOFoU zHMVq$C>tfa@uNDW^_>EmO~WYQd(@!nKmAvSSIb&hPO|}g-3985t?|R&WZXvxS}Kt2i^eRe>WHb_;-K5cM4=@AN1>E&1c$k!w4O*oscx(f=<1K6l#8Exi)U(ZiZ zdr#YTP6?m1e1dOKysUjQ^>-MR={OuD00g6+(a^cvcmn#A_%Fh3Of%(qP5nvjS1=(> z|Ld8{u%(J}%2SY~+$4pjy{()5HN2MYUjg1X9umxOMFFPdM+IwOVEs4Z(olynvT%G) zt9|#VR}%O2@f6=+6uvbZv{3U)l;C{tuc zZ{K$rut=eS%3_~fQv^@$HV6#9)K9>|0qD$EV2$G^XUNBLM|5-ZmFF!KV)$4l^KVj@ zZ4fI}Knv*K%zPqK77}B-h_V{66VrmoZP2>@^euu8Rc}#qwRwt5uEBWcJJE5*5rT2t zA4Jpx`QQ~1Sh_n_a9x%Il!t1&B~J6p54zxAJx`REov${jeuL8h8x-z=?qwMAmPK5i z_*ES)BW(NZluu#Bmn1-NUKQip_X&_WzJy~J`WYxEJQ&Gu7DD< z&F9urE;}8S{x4{yB zaq~1Zrz%8)<`prSQv$eu5@1RY2WLu=waPTrn`WK%;G5(jt^FeM;gOdvXQjYhax~_> z{bS_`;t#$RYMu-;_Dd&o+LD<5Afg6v{NK?0d8dD5ohAN?QoocETBj?y{MB)jQ%UQ}#t3j&iL!qr@#6JEajR3@^k5wgLfI9S9dT2^f`2wd z%I#Q*@Ctk@w=(u)@QC}yBvUP&fFRR-uYKJ){Wp3&$s(o~W7OzgsUIPx0|ph2L1(r*_Pa@T@mcH^JxBjh09#fgo|W#gG7}|)k&uD1iZxb0 z@|Y)W79SKj9sS&EhmTD;uI#)FE6VwQ*YAr&foK$RI5H8_ripb$^=;U%gWbrrk4!5P zXDcyscEZoSH~n6VJu8$^6LE6)>+=o#Q-~*jmob^@191+Ot1w454e3)WMliLtY6~^w zW|n#R@~{5K#P+(w+XC%(+UcOrk|yzkEes=!qW%imu6>zjdb!B#`efaliKtN}_c!Jp zfyZa`n+Nx8;*AquvMT2;c8fnYszdDA*0(R`bsof1W<#O{v%O!1IO4WZe=>XBu_D%d zOwWDaEtX%@B>4V%f1+dKqcXT>m2!|&?}(GK8e&R=&w?V`*Vj)sCetWp9lr@@{xe6a zE)JL&;p}OnOO}Nw?vFyoccXT*z*?r}E8{uPtd;4<(hmX;d$rqJhEF}I+kD+m(ke;J z7Cm$W*CSdcD=RYEBhedg>tuT{PHqwCdDP*NkHv4rvQTXkzEn*Mb0oJz&+WfWIOS4@ zzpPJ|e%a-PIwOaOC7uQcHQ-q(SE(e@fj+7oC@34wzaBNaP;cw&gm{Z8yYX?V(lIv5 zKbg*zo1m5aGA4^lwJ|bAU=j3*d8S{vp!~fLFcK8s6%Ng55_qW_d*3R%e=34aDZPfD z&Le39j|ahp6E7B0*9OVdeMNrTErFatiE+=Z!XZ^tv0y%zZKXRTBuPyP&C{5(H?t)S zKV24_-TKpOmCPzU&by8R1Q5HY^@IDoeDA9MbgizgQ*F1Er~HVmvSU>vx}pZVQ&tr| zOtZl8vfY2#L<)gZ=ba&wG~EI*Vd?}lRMCf+!b5CDz$8~be-HKMo5omk$w7p4`Mym*IR8WiTz4^kKcUo^8Hkcsu14u z`Pkg`#-Y^A%CqJ0O@UF|caAulf68@(zhqp~YjzInh7qSN7Ov%Aj(Qz%{3zW|xubJ- ztNE_u_MO7Q_585r;xD?e=Er}@U1G@BKW5v$UM((eByhH2p!^g9W}99OD8VV@7d{#H zv)Eam+^K(5>-Ot~U!R$Um3prQmM)7DyK=iM%vy>BRX4#aH7*oCMmz07YB(EL!^%F7?CA#>zXqiYDhS;e?LYPTf(bte6B ztrfvDXYG*T;ExK-w?Knt{jNv)>KMk*sM^ngZ-WiUN;=0Ev^GIDMs=AyLg2V@3R z7ugNc45;4!RPxvzoT}3NCMeK$7j#q3r_xV(@t@OPRyoKBzHJ#IepkDsm$EJRxL)A* zf{_GQYttu^OXr$jHQn}zs$Eh|s|Z!r?Yi+bS-bi+PE*lH zo|6ztu6$r_?|B~S#m>imI!kQP9`6X426uHRri!wGcK;J;`%sFM(D#*Le~W*t2uH`Q z(HEO9-c_`mhA@4QhbW+tgtt9Pzx=_*3Kh~TB$SKmU4yx-Ay&)n%PZPKg#rD4H{%Ke zdMY@rf5EAFfqtrf?Vmk&N(_d-<=bvfOdPrYwY*;5%j@O6@O#Qj7LJTk-x3LN+dEKy+X z>~U8j3Ql`exr1jR>+S4nEy+4c2f{-Q!3_9)yY758tLGg7k^=nt<6h$YE$ltA+13S<}uOg#XHe6 zZHKdNsAnMQ_RIuB;mdoZ%RWpandzLR-BnjN2j@lkBbBd+?i ze*!5mC}!Qj(Q!rTu`KrRRqp22c=hF6<^v&iCDB`n7mHl;vdclcer%;{;=kA(PwdGG zdX#BWoC!leBC4);^J^tPkPbIe<)~nYb6R3u{HvC!NOQa?DC^Q`|_@ zcz;rk`a!4rSLAS>_=b@g?Yab4%=J3Cc7pRv8?_rHMl_aK*HSPU%0pG2Fyhef_biA!aW|-(( z*RIdG&Lmk(=(nk28Q1k1Oa$8Oa-phG%Mc6dT3>JIylcMMIc{&FsBYBD^n@#~>C?HG z*1&FpYVvXOU@~r2(BUa+KZv;tZ15#RewooEM0LFb>guQN;Z0EBFMFMZ=-m$a3;gVD z)2EBD4+*=6ZF?+)P`z@DOT;azK0Q4p4>NfwDR#Pd;no|{q_qB!zk1O8QojE;>zhPu z1Q=1z^0MYHo1*``H3ex|bW-Zy==5J4fE2;g6sq6YcXMYK5i|S^9(OSw#v!3^!EB<% zZF~J~CleS`V-peStyf*I%1^R88D;+8{{qN6-t!@gTARDg^w2`uSzFZbPQ!)q^oC}m zPo8VOQxq2BaIN`pAVFGu8!{p3}(+iZ`f4ck2ygVpEZMQW38nLpj3NQx+&sAkb8`}P3- zc>N*k6AG?r}bfO6_vccTuKX+*- z7W4Q#2``P0jIHYs)F>uG#AM#I6W2)!Nu2nD5{CRV_PmkDS2ditmbd#pggqEgAo%5oC?|CP zGa0CV)wA*ko!xC7pZYkqo{10CN_e00FX5SjWkI3?@XG}}bze!(&+k2$C-C`6temSk z_YyYpB^wh3woo`B zrMSTd4T?(X-jh`FeO76C(3xsOm9s2BP_b%ospg^!#*2*o9N;tf4(X9$qc_d(()yz5 zDk@1}u_Xd+86vy5RBs?LQCuYKCGPS;E4uFOi@V%1JTK&|eRf~lp$AV#;*#O}iRI2=i3rFL8{ zA^ptDZ0l6k-mq=hUJ0x$Y@J>UNfz~I5l63H(`~*v;qX`Z{zwsQQD-!wp0D&hyB8&Z z7$R07gIKGJ^%AvQ{4KM0edM39iFRx=P^6`!<1(s0t|JbB2tXs_B_IH9#ajH0C=-n+ z`nz`fKMBKLlf?2AC+|83M+0rqR%uhNGD;uKA6jOjp7YDe^4%0fRB<^bcjlS2KF~F; zu09wh1x0&4pG&76M;x8$u`b134t=dEPBn6PV|X29<#T4F1mxGF*HOgiWU8tN@cguI z_F@o+XL7FJztR63wC|j4x_DANzcX94r7Iz-O2x$({&qd*mdLG=-Rv)uZ}UlMR+F&q zU}=lkfb0p1>1Ho){o$@}mSKIV;h*$AND7~Dl)QzpFBlSM99Kx+F7GsVK5xcR? z_4Q(Z%cgk8ST}U;;=!LwyZVu^S$>B-Waeik%wzcKTIqeX=0FP(TGQ=nxi=dsS5BYF zl@?}NT!Y!Iyos^@v7XWXA{_bV~1lxz7gC?xuXxy0_?GaN!AhRRM5>)^t%&ODd;@HN5L{MD3 zc>i2keQZVm#?NrDwbfd}_<*5^U&w0zv~n-y8=GGN-!=_`FU^cM8oVCWRFxw?BM^YD zi=Vxz4q|jwPTg+?q7_XI)-S@gQkh>w0ZUB}a{^ z_i;`Y(~fvpI!vmW*A^|P7(6+@C4UeL2WATf{P1?H5rk`5{TL zcf!CgP6Mi{MvjZS)rfo7JLDZK7M7ANd$3`{j9baD*7{#Zu-33fOYUzjvtKzR2)_T1I1s7fe&z|=)QkX;=`zX8!Byw-veM#yr;|wjO^II>!B*B z0+w%;0(=*G3V@88t!}~zx)&do(uF=073Yeh*fEhZb3Vn>t!m(9p~Y_FdV3IgR)9eT z)~e9xpI%2deTWyHlXA(7srrfc_`7ACm!R>SoIgkuF8 z!wkOhrixFy9y@)GdxAntd!!7@=L_tFD2T5OdSUO)I%yj02le`qeQ=yKq$g^h)NG;# za(0J@#VBi^5YI|QI=rq{KlxwGabZJ0dKmfWDROkcM}lUN$@DV`K7fU?8CP2H23QPi zG?YF*=Vn=kTK*#Y_{AQN&oLju|0#E=fx%YVh>S{puu&K$b;BN*jIo@VYhqPiJPzzM>#kxoy0vW9i;ne2_BIG0zyRFp<3M(iY(%*M_>q0ulV2K}Tg zkG{EWKS{i%4DUuHi%DVKy%e+Q!~Uf`>>F6NgD{{I8~nO4!VgOvtFOc7(O)X`|7n*f zxBa4CJ-v9fUUH+`7sPVvpM_C*udZ@OTGTzx56QM5y~OlrZc&w9=)B?nmd@keRn+^= zvm~4sa5987LFDnU{(N|N zJAR8H@}p1fC+H(yTI4n#%~TbImMpuqYn9cQ<0QQ%=PzZItLkC*ef9WJUvfITKWh#D zc#__8`4am9%#NslIUw+<82#SR8AYG|woLfBg#!-&dqq}@P>|I0%lbdy0lSMmNe+}o zj0zZuFr6Wb?Y{Qy-S=|r`bdrDmhnmvkRnkdn`YCleU>Q$=je}LGhh>_QAj6aa_0Oc z%Swsmui;IRx7bN*=AAS@5yW&Y2hy;3&|HAiA8}!HT6!Z!RVn~MZg`RmI6&%#tBZDx zfD+y@Z~NWlk*4l13vmt3AK2wP!fQlnBbECL>?p)F?T)<`w&QN>cP_V>r7UTcsTaaP zTOb$f!P@zf$6>890NVKbIkG8rE?9!Y97sMSZjfF?A zYR8lp`LMoz~O?iaZN;gcX;LC-%Ia*R%A&SLx!YIf29?P+=XAAojK8!^OU*@?R&DK!#G_lsn!#;S375uZ&B0HH1|BO0R90$U>qs zSvHv>H~mAgNCcjo-e+;RjY6B9NCbQrZ|BHjTkehaU<9CSkdd>Vl*ifA2LNOP&R2Qdy3k3-TQ+ zbq=#vI43x`s=%~cGyN&y4Y!FxhwgDe@i6uv8^BLL&3z*SO=D0aLjih?gY4-9uWp5or)H+v~w6n5X#F-I52z=Z_p4JB(;M| zeaVFhuR2|3UD2MzVc~^nSoD2(dD#uL_1PdnIxeA{V5n`#3xf1Zx@4lw(DsQ&H$h zw#%3O<1173hjg2_nhKi!d1ej=h7y`hVjCNB6|HTnx>SWuCE-kgTnfT+YGX4_Lun({ zDv2`>d3vrS)tTf7ps_vvh!Cx^e1BFuWnEAh0(7fkNk|-3oU|iRWdsC6U)?Raft~HN z;^$U}vZK5O8|LV$>6X5T(uYkblv{zwPxnQBh(BQ5tA~J!vGiAMYP^_ki~pkIxDfOZ zUJDwq%O~WueeV6%uN<54&u*c&E4y431cklBNrb06zGOOy4XNT~JS-q(s6@)F@ovbe ze`fial(O4(-su%6@@1+V0MsdLLMyE8;)nou(7}czU(5ASaZYDT(kUZ0L(&g$nF^n9 z9-Pi`ZZLX&)^*M6As4_2Mmc9S7OT)F8KkL2NJ)KJcnCuWU=Wy402A&45#Q9Id~BBH z0cY*xlv!uXzKrXLH!xQu(OtJvEj|0-DmRj1vjFz{c*I4$Pe(+_V|^b~S!0xm{8lq= zZv)@NlcyL3Xdz+*|L137F7y6L-2VsrKw=q^S>F6i%<{Fr8zk06$Ay-(!L$fY@7mcng!2}L0t zgi|KxfB63Xtk_Q8#ZPipQ@!zgjdpEIbK_?q17Hoi4Eiyun$hrc>T(7pOLVLQE=lgGwA+A308p& z7@=09(|$>eLy5gLe{*|3b(M;1n;C^~v?o88jYib48eR4$QGsBFzd}3QuwO^_XE(=B zq+hMi0UFC|dB{LCwch7;zYT=NK})O%sgi0k#yV;My@24^B1+CuZmYOh0^b)5Ba_)) zC%i#_Iev&nsu%I|1N5=MVc#PrlunKAs&hY|3s5;@}`>sB>}gzxuB zB=2vrRyB3uiyW(hkDUNe1@&(b`;>ZvGgw|@s{zVC#_`HXIN_^J@Etb zA7A+F?ot37T{<-vTy8h&b3e+WKHE1oh;pUQrN4yRRrx?mT_9jRa2i4l1fUnLW^Cbl z!I1>VzyFe?VELWWhM?@?t-YPZkD-Qjo@bC2(o#ZtZmr{KZsdFWItV`rs$gp{724@C zL8K5}E0+DHcWcL^{BGei4>@J-3%a#$y6;I}=upc};-NDv-z#kPX26ylOpH)Ov1uU{ zkLj6oiH6l_s+B~_z;|Jc2oi?naS7#3H63~~lWj4rUnd=fCnKdkik<@R&kch9q##G{ z4u!%=rlM~Yp3jk*t8}1B`Sv6<%Z^}~1e@aq zg|JQ`QO2pSjAm-g*?IrNc$^~sIrNBo2$m|Sxanr?Mfs>2@Auu49 zGXlsS<9XS1&8h(dD*Hl&5HBDG!^pJ*lkau_Ur+7`7z;rcs$hT4we?3bT=7Fe<>{5( z2m2(c+hUz2BTHM8dCe*Z3XX&Av;b~a=$6EF>&^E8%nyxO@m_n!q&XD^A{SRjRZQ0L~qDeC=j&0$j6=LNIz@`ni^>ch|sv}^6 zlm>?28yPl@WmDPR?Y-A9X{U9Dv_IsbXJnzKCjkRksLOg#42uG2mE_acbTQ4)J|1V>%U@K(FP3AYhL0U zdeOCPN1qLv!|#c=p!_+%VNV(GHt`RuLRV^vz<5tt-r)yOK**kUWPspVAf|}ZL{LS= z@k(@@!P&W!>wwe`x{+GrFSWhHov7hu?{KuuT%kl#WO@*WX$i_@retlhQBj++SVNCx z5$78LxP>Z=^aJ)D280r_jj=zFfMJFXCIe^B{~V@d1rl_F(qo&AB4bC-vYL>x2jSKX zpuTG-6kgp3e^T&+dtV*i6a~)v@n?n*MffN59y}<0djUX zt27R+SE#hp8bzc#;rk$jw3r4)Q@eI$*`_)=Pvge8@8|8>H3X)<9YX6cXa=ii#Le;(qKm@%0-7$>2ShnYc`j#zJ7gu_FE^?uAkL|H)UIH#gPu^40!6^J=^ zr`}iwa^!4tzW~vOMZAaKF>*8A{^8m$i(VK)>?=#l`xrVe>wseSvM_aF zATNkY>kM_P3?1kE`uIq#mvr-wuTgUH0N<&JhF=(E9%^NS*HLm!4GZ4_XI zL=R5tlG5Mk_1rPfg)sk^llFuKPMPBhuU|L5q#yP_mzxp1o&pAzi-X31sgFpIHn@($ z_>=`AB5(8tP6p2zS5VEvH5J$M` z_much3>S7t3Yo`Yx!>83-hW9LYzDKP?mKdkD#QAK8*M((sx{eBQdrR<^3ZhFP81+& zBnJMUefQyNBji~$5d88Wfw1Lv59aJN9t2!pABLg;ewJ#LXL-10;QcJl+Y4Mtngb)k6JZlCf)3uD_u)J3sYyN;NN5hNbg$%W!i-GK%e&!Us)2IExWSss$YG(hm3kJ-h%yD z>8q^n$+4I(_y_mbT{du4P%h1j3oSpjhY97{+IZ`aA4ug!vNJ6*p?<2H(2w+GD3j$I z1TUXGyNzdf>_yB3grP~FZUs<2Quw;eEi*7s(-MiIkQ%@J^+WGdQvYSUN+TRiD-xto zJ=OUU+kxGYc!HCLNbCvR4lGTp~#L;DFzGd-#gJe*xf(P3hDQz|y)?b9mwU3WUVnpcqXM<@w%r-k*Wr^gzAv)8T^sqA=Ye z!7qy&exJmAcAt~CwS#@yNmjr8*T*!A6w4~E*ibaLRs0CFo(;R3=ODhDt6zWNodmo0 zXx&bT$6&+5c>a|WJ)F4G-^GjY0H#*tY=UNyYr_q5fsrcjk(c^~e*7Lf`!Jd`)p412 zn|^*hV= zFI4UbwA%X@smDd$cQOiMC%jfitTxTb+#`9`G=2rJDfK!E=5ra|So>lc{X1$~w28i+ z4p&cTGwZ#5VueiXS9O8#;RR$yg7tL9!^)Sz&pZYIzlSh}0}V{LxL$Cu%B4U5_}k}- zm~|CsD<076x@<>m=6w6N?WaThIBP`!u{-;WF)xc=2otx*lwf|5+MkdJePjh(B z9SH+%cHGCMAXNxB{_3^otDWdsV7Ob6n{0 z+&!(;iaHOX__5z_$Qk{%xYV%Ig@7iokGBwR`3642ZP#H#v9QGbWl8<|MS*=@qO@Uj z6+SZ_v9`1paUe5tFN~v(b#J3a_Lx0+;r9giZIx-A5TxdbG>xi#AZ5_z1V}B^n)sxT zz49}eK7EWb6wR!6-qQOrHQHkUvshvq%=G2d&@(#XM*Am1;WbnJ{X_!a{ZkphD$^TQ z=Iskb&}=lBm(RHiwJoGg`*NiQ6#RB$T#LF+>#ef;Jne&MxKPX!#r`&TVEFsp2jnNx>dClzpcPy&G&13a_<0qaR3i+k212~hoQ z8nMk{JP-t04I{GW5gUBqcJW-jSMrlw}>p)ptx?WKuCUV77taMiV zHok9V=6yv+Uts@fMY&A}amC=!Yj}eL@=e%XJ#%?agkt1jWF+10{(E9mHLDa>Ll7Vj zG=3cp%ljIB-6pC}6&`xJ*6WCP|IlglLWJ^?yviI8Ve)?V_i4%n;olzny62_`-|IGi z^=}p_O>Z8M;c4|RExu70E7ePW(HWVS&E$+LL6xSQgB`QfMQJ|4pCTFowA39p5P-|$ zUtM_H2HnP8_RoS~Vwk(FhbG zH41licj%=0a;Ln2STFBvU}Ne&O&%8bYKj!h1FA#sNM`232fX|U3QPp#3C?mN2;hE9 z;)!@5ixSPl<89^7gwhHc2YAX1KJK$#*3`KOMIQ253q7-*RJ5k)zp9GBO|Ga~X*^}US5oN@aG&waHV%vi~r{t^`ptTxb zL}q1W8S7*>7oWwvgV4uFLZ(@k`R*=LO_|Gu`prs~!WQXj-NLIa^2(7IHg>BG^N zc|i{-^=&Cek9dkJFQys|sjG9i>LLz|;yCv{^1i%c*h>8zF91kLvS9HBQi~ZU!JL`B zK8N+U0fr1*6??Ium)AF!6tc1eGhXIYL6IRT7rmKp7+>?%5Pa6zC5)KY$ycF0ZJ`G5nEQDG100U-jLkH8^UE4g6wq?sg%pP=-$&G#bcN`^?w3a6 z((s$6eRKcSEIslW-kk5Qi|5Mg-(xdLF}PxxVh$PuO}#aR6pW1kV4Af!Bqh*btXNNZ z>-4(IUl+L4dw+3LcpGut=qB45O+W)Q5?*zZ2A6rJcg`qkSvWA!j^r2mqKuCm6`Py? z@^T#Ux04HemPGd!Hs7NkZdVn1}8_j`o?)*OKZGS!`ff)gF zG?v-lj$wWNWCcw2Mg2o18D~1?3_b0XzdiKBNkYSDpcv@&kp0POmweJE2ZkIQ3B!a! zIgIoE+Xv?;34kyo^QYjZk+tEqZvq^#QG(OzX4~X+KtsoQoddTWUR(yo8R+ObEF1j<-syWOb>)JQ&Zbdu(sctU%Mt zW&YR0{ttY2TTXYZ?~WNU&cES1Z2q(7SrWDh``!J(JM+Nk$!hu&Y;(7E`ZNKTe0w+% zJc?Qnw2B+%UR}0;cB0Rufa(7-3FF}?629@LgTiEC&2uyL6NxexOp?AKT^aAx3gi(W zao>r>MPw0eQ3>IV02uLsC@>yK_epX6GRg4{NEL2wPPF9=*L2RV3yyK8DhuEK>rmmV z`&Q~#c`lgR&93TdOCja|ewOXmPNRh7!&dMT(1ett#iDr8HZW~VqWW@7fe9B6;7S+? zbC`d4@MEau&mKlOPKd>*10q0c{~^baw6!a*w^sY#0Xim{oOsiXiDOhbG&kl3c$$n1 zMRrD83&QucDSEcV*7LIp8VTA@F<%qe+_c`L;6on(>SjAU^}5c9!BCffT>$VQhe=)z z8(=Ej{5>jhmjB3{xDfj2R@VmHQ!CqjlO4KnuOmvHy3K#po$yp_V;p_MKjh1`(rzj6 zHW956k1yvntz{_g?Xbs`avK(IjlTnsu%htO;D7 z?J#x^EzuvVn&NA=!MEj7cwe5A-Z$Zk2LBZH$~%E* zf`((xH0?`}hs|HA%mtwfOEsZJxxrennkTYcwP#FKO5%Lpc^JXhSpV|ZH$Wr;`}`_( zIP==gd3LYyVtwD|*ZJGi{7~x8{=^bGVqu0RJ`n_BZH9+}kz%-4ZRsImi@rx%=ZEKs zcPnUXo6hbJV>fH;@1|bAHIe0ijYI*&kdT|HkDS$9No9 zCHo=*HWb~U+Dtzxr+Esao}6@|;Pf+E$ay0$kQp#s{wlw+7aIKbMdf`OqhoG*;Tco0 zjrP}VQG#Y2cJuqoJg&5({)S(BA}q9T1lGeWRyu=Je|)I!6a+aj!IP^1({)ZYe&x6w zt3a)Dq^TB+A7CdB0-}#z2Ur$W&h3YVw8==!xONy$uQmDWh-@15iEOt!q2m&?ZLA|w z8loSb(0}7y6Xu0?M5Uf4>VZGluB`wMf2oh;m)ghxVda>3m}4%V)r^0nVQ5V6f3>*) z0&VN!N0~GC^P}vj$`EDMZEmVV;N&RISY2C;$0;2(<{Lt&PKzqRByQdiEHGAbwtbS zPj`Da5%U6k1oEtVzI}QNw;!hT6F+~|@=c@$C4NtO@=xgP?|5MyZAyuCzcvq4rdAv@C06%gZ`9%I);R6UGiGJobfux+<0DLS&|MSG4UH z_~o{^^9>ixMg~mY!-@Fai{xaE4^;qy9iZN15Gbn5ZqHWf>Jc5Rv6(#n8`1NcCsdmG zab*dSXVPaE?)wCalD;$ivF%@nB#7D`@YG04p6ed9m}4iJW|pfVMLE<-c{=-8$e?cH zUdU#mCj4gb zZKA^b9p*9S(}8@tw~1RNPHr7tQr;P+-)D8|sq=*o)G%RGqt> zzP5yf`pVxb)I51D_G~Xp^GNK zVI6sAX)a9s)e{8N3?35YA6aQTXuyszK3ah~CemzA&CII#8F&F#KN41~8I^&_%}6MCNb{W87qAF`zj_Y^szhb> z3p3}KbOxotY|(lD=;)`fYE_*{S}x;f^SW#)SU&5X#o|-R|trpa|L5PS5aa0 zTHw8%SDSVtU4?vyrhnq+^@dgFS)|(y{~(4j%3UEiO-rBM9%`)8(dh33pMLiuurNY# z#10AsQ7%*0Cu_DSAU}P;X(JwA64~Q_^R%d_zSm^6Aux?Pn70PM>9EvLeOX z&w9c)pGmcL22;MO3C_B>=NC0RJpMp8?#ZUf=GWRvy z6RHq3B}=MGVg?9@iKFBpsvnkVh3{Vpp=`CcD=u~@ql{my|6?3ssi3mCOPnjI&E}VC zc@X+Yl>;;DNo0W0`0th!X{?luDhOC{E8N=?!w}K1{V=)+1={m(f`Oc|N=07>}3;z{-(A zm{JL=j?Sro5iecmE2-pWlRf(r%|HEQ7kgwQ9+kt=NBhtQI7OwcZ#3%$Uf%^r2nhjY zoQ08MfC%_X{O9~WcirMZMhn#z^ux4Erx-tf-6bHD)9eH&^L>^jvAd^9A^DCDs?0;k zkm7LE*KjP6`2d17MrQaaLqd_Rka}J$csvUec#hw78<=s(hyR>065~YCVCA9+#Q+; za(*L0IEw!r5P|@-;x33L$Lv9 zcuN8YG&g{<(SeJG18~(b!5yywSqQiLAX0;---;}mF5&b4lg|T?LwKREa{9YX_-zL@ZE?Zqi@HxK^2KO1>0LATu{te=T zprmHtY)bDVfxI1S}KBE7V zznP7KQ8HekWU#W6mw`dr-boV}pMQR==&5=Q5T=_q091jfc;R*jX#&=MQ%~@E@9^?`$v48ks<>(fI(F6L(5ppKy|$HWng*bKOb(4|cMUB&z$#ob#XV z5-mg)gmFIybZf=znm3ZPyUO^GJfxt0kmHjaTZ|sthsxXw&}Y)fOUSg=JhRSR^UjZ- zhqqb}Wsyw4zdnj6@#BAJa#-PdI4_dgafFXh85DsEQ_cT+5)XpZq$fZlBA_9UsE9r6 zEFec5?uqN@QhJ^IzwZrwl-5J`CmVPv{(YDTqEqWR^dI;5hXc~cxP%B3v&~s0`Ct89 z@S`i~a^c%V^N81dDT*ItFS*&IN;@O$EgzX0e7x&}TD=!zS}hTpezBLS>mdX(5< z)8DEI(-o_D)c-UX@dA1MuJ*yc>Hf4|`*B2S_O>w*-tbUwtiu`;W(Ud{HTty@(&x(T(F&;M zJ=?H>6`B7nf-90e8V`WSVp|0oEKB-P2M{}4ZDawzvM&a!y>`Y#jCsD%T_l``@ah(I2nJs~Q|%uSKu@k!m~*8B*IoA{*TgtF<(5sHCGG;n@NE%~Xt(G$^&<87u;}Na zx-8cq0g`uA(&RBFo=-4Y1GUZ<``Zw{xL4jfHkZw~%~wvtGueszcXt)_QwH8g!; z%s&3kSa~R$dO$-%L-)c@_hi7&>{6L_M>OZFkUQu;{sL_bUMStNrt{{&O(Wn~*zPOk zB>dnfszb29NSTf2pqIs68k|p-UrSrxgLHqi?3N-UFa!LHy9n1)=s>`yS+J{MEzS@ zNlfGtpma7kG&LR3JE@wB%rFA*h~~KitlO=IP)ZjN6dQLM6qsry zHkB#cyNh#n`)}bCrN1My*;k)^@>e4gJ`LJK?2)Pwp?4Tl4)4FA0(tvY+#1jOUM)xw zlMz4x-f@g^+yKUN`?Vu)|AwujArnM~Pa@y*Q9S8eS(u{-S%(Z5=R~pRl5ZGDjdqH% zC8rW&{##wOpU_oTIG4WXMk4&%2t1;lWcW5&!yxmOT*!hBcKyTqEcNoO+R2;Q?Yj+W z1-Y4?59fijz4(MIDwGe4-baYf08UCs;r|YefD-Md2ST;=cxwpgW=tR76-dQVAhn^= zG9Wk5lQk%jIR@KNU!UMp6@BfU;r+;y4VQ)D2!Il9HX%yW-9nOzV+m$YKzVaO`B8S7t z$!S2Mz`xw>V(RjE`0>bQp<0y&h~Y=M#jpy!#=dE>`=e_AjSZq6u!Dy1xJf~-7|0F! zPR9|n`e_7D2DIV2H(CESQ}hA>U>n|6`%z?YKEA~)BOVY%y=jPV zT=44R!L?J)736X#csn|lfBJ)o8ixaZclguWgrGO<`TN2FMfO}7;5}d+BlK0yTSH3* z4!=;5rOh85&2|x=46hkNaz?)U8&=bcfh=N_#8BNpZ2v$aVBo;sk^*X`v;4-LU;D>! zM*h12MxXIQy)SfAqE4;jY)wgnppazZkdNNVVF;(PLf^qK$FgY9+VFyBKE7UC|f z`R|?&egV11K3s$rJ6!GvoeW=jV*!-e(wA;x(2=d0E_e_%0x--0o8#~m^H1%AH5Z^B zn!TNPn927*bvaf0pt}zhK0o^V@WlGwwKo(*nQ|Q~4_;>~-8y20`HP>@UJa)3nEnGG z5Hwhs|FcmFG16ZVNb5hL`2Gc1{zWIMM{_OiKewV!hCi}U!VuE?s9wU-QbZ!)+Y^tS zGzp5OSi5iq6hmEr$w}&9DFgoB+i*`q`8TBi^MVS{SKEb8Aw%@K7@XCo(De2A`6%mf&a2#~y1N)+kJLD$1HCP!22)(U}xo2|j?WRzt(11j8Z_*v;P$R+Ug*Gy3VxV4K; zGGUGabnW*`Z}~`ydXL-l9e=GC$pY#z|63vy>E*m=$=j}iWP{sRTh0%H54`t>2xYH% zsk+M&u&pNgMCM@3e)Xc?jBWX-TIR_cQ1Z!RW7!B zBjZX=+^3}?SE)B+$EP+0oi1Fp5blDT?*}nsP>filqXH{ms zxU<$hetC`u)Wi+x|EKL-`y^#aQX+sDYIa{M;V%LqLrOk~lR>u0Q!+pyQSU4zY`?E^ z|5@)C)w6G_=i5YYC5SE_u(7hDNYr}uKT|@DSqF%S++lTIbIk^$a>{~0IH8KNFEy%+ zW#$&!ynpgNJh>6uR~?2c)ZMW+h0OKu231(7L_vETPaR+(P)Zy%0~yGm>E9?@@x!Jy z3PYgS}Q@b}x}E#F27@F+j}0=&Ql4gES&f8acMrPAVlVs9$97`FR))R5wI zc&}KFI1UIewh>3PkhnB7u zS3AT8_*|nexznG|Z*DU0c!K@jsI4J)5#DyNi#|e#`l1Vv1`1)*NVcy0LZ``aL0n8B zecupJ(rhq3u8bW0NIRhKYq$v1li+jp*4hfAd&wxYDE8vn1TQ7S@bTM|I2Ob z8vMOIxA7&_j{AKmD+O@EyXT`|dElt0pED^@IV0m)RPBUs*5jW60>>w1!@_G3aBKzG z_f(KfAPBk}-jQtR*Sroq!*3rbQ_m27e+YdzQjUb<_*k8vc_C)y!@cj5E>NxUhPu&g z@Z2<~esU`)ih+4opWe+K7sbN9n*9@n>#@n3*o z?xoROgDuvhq>jJ;Ve{6i<3roQNfgo5^4Q4(|GNExO2Dr7GjgA2zWuKp_K)K0R(6lv z!l$!zW-+T6mb3gQaAFviTQi{|*t%>{(mhTdy+y;Re4qT@kccy#{b z&zWy~kLO@>*WPj2k#H)|7L&gAJ37DmHQAme#@m;(Y8Nu^`D5vf8sZFW#+lA2!HK=( zJ)#hO6JD*`o~&c*&46d}g=Qj@SsoB5ikC z^1V8E+&<-OzuS_C`p5<<(A6fB`LXT(!kV^0_~hL6PpW4={l%|#xgdh?5EIk~lu8{D z2hiyhv3Yxij_#$Wu>P@7SYsl`-~3;}Ktx{34_NL^Kwin&=?!HDv3elQDbcU*qyYpN z(#yw~f1vFGK-t%CC-qa-4FYHbA^h>bag-I&*qaxwn?Qv|idE$<>1H|Gr6JtUu(he2$eg!N z@HTF@dG1)*y;4fxe)4_ZkpaBHH9hXp9p4|gLrRQyuevRd@gSS}JhRnWqrvm|U@>qM z=yl7RQROTKwQtzP3!zUF)_6Ld#NGA6v~2{J9Dd`h6{%+XsU#qGLh%`fB1Hc?wfayK zN`H4BpDp)npVQuu$DVW1qsBS&AJ2eP%6Qw>;k{)Z$8%HL=Q4(a$Ng2_vHw&vA!1L+9zc8vaX2GtqJ{L-;gvF0IR$em zMQ8@{Qp3+3Quk)TJ$?I<8KmwzD*7#(q<@Mc`dchngW}cRG14(Z6K7{T|LhFXwhqUQ;BET;cYqPcAcMgt6M$V9$(?jHo@Sud$an$U&5F zZ1QNh^ztt)E*d#Ij;<43oSKKnd+WNr$_r}+s_O_x6DZSB10*5Q{ourqq>mTl| zx4y^(cy+9;t@R=*j>3_dmm_m)$k$#937V(sllby&5)Xex^UD-|m|q<(jEd#@DV(of zAd7sSdmS*zUDqJ9|K%O2J2OfdUiK{{b{PCy)pi<;hp~7v1CQj&4-10 zgO<3dqhYH1#-Fa}Q{pjql5>>P6gZH21zLfxZ4$SK4T@7b!|`nWF9b*84Bq8&Eht;9 z*P72x&NUCZ7*@B$`FtE=hz5b}S`|c6Ey+j@D1ZibjJaRlR;{cxAWv z?Nqa>QqV*H-*zzaPvpLMHt~nl(x6?vrPpR?zn7~wow?oj*1TKmx4j71>$hvtC$DLD zUrz0^tiP0792U&dxJxNv@r}Elsjn^aSLUu=9#mD{&9n8|ayIL$!H3s>%KEvbchBFW z%cd?VU83mGF#Dar9*s~w&AnmQRQIOvR+uWsuZ?+|a=TzApXO@q^(r%8=}iv#wCnFq z=K9}JbqU@k99Q%j-}NNk+qLCP)jXfmOO|)@?mHcnynd6({mJisP1_}u7k)|eYHXWK z63eQ)E$ufFi!3CWUY2gw%e>omCv}qEX66aH-k&35f9`Q@Us|NPetVqe8=dX*VxJdn ze`q7b=Dn(UA(2sf&g)cOmQFhNJ#<-aMELJZbA#@to>25@kbW<)&!X01 z%NMJt>1ST)tyX)h@?`DxhbgCHr>S4wv}WC&Nw-!{+Z7$2D}74QAcXTvip=M0%Tp_N zor=k`)t|ra^ySr-+(|R9mB(E=`MX#y(wSw)$!iymzB;^c*>%&^*7HxTnRga=soSZT zdDl+9s;r!v8hk6POtzBaig4pRp7eWF(<8gufvNHPu6xs-=e{;mnHzJyGKE+8L0j}; z@%8-e^UCL5HhMiR>sD3Rve&yVZ#{Q1*CO8c+qSr^Z#CN;)(X5>tGG5yUw3<+CfhaL z%bP;hZ?jvgJU67BWyiy74_)6r)_nSxttxn0`0?HE^5(uydHVgP+HE$V?Lv)Leti43 zWA|;f-RqX``95>)^P-fw!Vi{3KNsII-*5f){gdxqd%gVdB1sOBNe=nEW%;i~g_P8J w!5uhoe-Jcg1nPN%MiEAtgE$;km@@t6ukO)1^!cY^83Pb_y85}Sb4q9e0FIsP9{>OV literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000..47821fa6d8 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = appflowy_ui_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000..36b0fd9464 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000..dff4f49561 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000..42bcbf4780 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000..dddb8a30c8 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist new file mode 100644 index 0000000000..4789daa6a4 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000..3cc05eb234 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000000..852fa1a472 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000..61f3bd1fc5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml new file mode 100644 index 0000000000..af361ecfab --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml @@ -0,0 +1,24 @@ +name: appflowy_ui_example +description: "Example app showcasing AppFlowy UI components and widgets" +publish_to: "none" + +version: 1.0.0+1 + +environment: + flutter: ">=3.27.4" + sdk: ">=3.3.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + appflowy_ui: + path: ../ + cupertino_icons: ^1.0.6 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart new file mode 100644 index 0000000000..423052a342 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:appflowy_ui_example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart new file mode 100644 index 0000000000..92d4484c0c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart @@ -0,0 +1,4 @@ +library; + +export 'src/component/component.dart'; +export 'src/theme/theme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart new file mode 100644 index 0000000000..dc85a3ee55 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart @@ -0,0 +1,54 @@ +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/widgets.dart'; + +enum AFButtonSize { + s, + m, + l, + xl; + + TextStyle buildTextStyle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return switch (this) { + AFButtonSize.s => theme.textStyle.body.enhanced(), + AFButtonSize.m => theme.textStyle.body.enhanced(), + AFButtonSize.l => theme.textStyle.body.enhanced(), + AFButtonSize.xl => theme.textStyle.title.enhanced(), + }; + } + + EdgeInsetsGeometry buildPadding(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return switch (this) { + AFButtonSize.s => EdgeInsets.symmetric( + horizontal: theme.spacing.l, + vertical: theme.spacing.xs, + ), + AFButtonSize.m => EdgeInsets.symmetric( + horizontal: theme.spacing.l, + vertical: theme.spacing.s, + ), + AFButtonSize.l => EdgeInsets.symmetric( + horizontal: theme.spacing.xl, + vertical: 10, // why? + ), + AFButtonSize.xl => EdgeInsets.symmetric( + horizontal: theme.spacing.xl, + vertical: 14, // why? + ), + }; + } + + double buildBorderRadius(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return switch (this) { + AFButtonSize.s => theme.borderRadius.m, + AFButtonSize.m => theme.borderRadius.m, + AFButtonSize.l => 10, // why? + AFButtonSize.xl => theme.borderRadius.xl, + }; + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart new file mode 100644 index 0000000000..22c5325681 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -0,0 +1,79 @@ +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFBaseButtonColorBuilder = Color Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFBaseButton extends StatefulWidget { + const AFBaseButton({ + super.key, + required this.onTap, + required this.builder, + required this.padding, + required this.borderRadius, + this.borderColor, + this.backgroundColor, + this.disabled = false, + }); + + final VoidCallback? onTap; + + final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonColorBuilder? backgroundColor; + + final EdgeInsetsGeometry padding; + final double borderRadius; + final bool disabled; + + final Widget Function(BuildContext context, bool isHovering, bool disabled) + builder; + + @override + State createState() => _AFBaseButtonState(); +} + +class _AFBaseButtonState extends State { + bool isHovering = false; + + @override + Widget build(BuildContext context) { + final Color borderColor = _buildBorderColor(context); + final Color backgroundColor = _buildBackgroundColor(context); + + return MouseRegion( + cursor: + widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: GestureDetector( + onTap: widget.disabled ? null : widget.onTap, + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Padding( + padding: widget.padding, + child: widget.builder(context, isHovering, widget.disabled), + ), + ), + ), + ); + } + + Color _buildBorderColor(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return widget.borderColor?.call(context, isHovering, widget.disabled) ?? + theme.borderColorScheme.greyTertiary; + } + + Color _buildBackgroundColor(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? + theme.fillColorScheme.transparent; + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart new file mode 100644 index 0000000000..ced936cf0a --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart @@ -0,0 +1,51 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class AFBaseTextButton extends StatelessWidget { + const AFBaseTextButton({ + super.key, + required this.text, + required this.onTap, + this.disabled = false, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.textColor, + this.backgroundColor, + this.alignment, + }); + + /// The text of the button. + final String text; + + /// Whether the button is disabled. + final bool disabled; + + /// The callback when the button is tapped. + final VoidCallback onTap; + + /// The size of the button. + final AFButtonSize size; + + /// The padding of the button. + final EdgeInsetsGeometry? padding; + + /// The border radius of the button. + final double? borderRadius; + + /// The text color of the button. + final AFBaseButtonColorBuilder? textColor; + + /// The background color of the button. + final AFBaseButtonColorBuilder? backgroundColor; + + /// The alignment of the button. + /// + /// If it's null, the button size will be the size of the text with padding. + final Alignment? alignment; + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart new file mode 100644 index 0000000000..31a3a20b5f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart @@ -0,0 +1,16 @@ +// Base button +export 'base_button/base.dart'; +export 'base_button/base_button.dart'; +export 'base_button/base_text_button.dart'; +// Filled buttons +export 'filled_button/filled_button.dart'; +export 'filled_button/filled_icon_text_button.dart'; +export 'filled_button/filled_text_button.dart'; +// Ghost buttons +export 'ghost_button/ghost_button.dart'; +export 'ghost_button/ghost_icon_text_button.dart'; +export 'ghost_button/ghost_text_button.dart'; +// Outlined buttons +export 'outlined_button/outlined_button.dart'; +export 'outlined_button/outlined_icon_text_button.dart'; +export 'outlined_button/outlined_text_button.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart new file mode 100644 index 0000000000..68fb341827 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart @@ -0,0 +1,125 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFFilledButtonWidgetBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFFilledButton extends StatelessWidget { + const AFFilledButton._({ + super.key, + required this.builder, + required this.onTap, + required this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Primary text button. + factory AFFilledButton.primary({ + Key? key, + required AFFilledButtonWidgetBuilder builder, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFFilledButton._( + key: key, + builder: builder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + if (disabled) { + return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; + } + if (isHovering) { + return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; + } + return AppFlowyTheme.of(context).fillColorScheme.themeThick; + }, + ); + } + + /// Destructive text button. + factory AFFilledButton.destructive({ + Key? key, + required AFFilledButtonWidgetBuilder builder, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFFilledButton._( + key: key, + builder: builder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + if (disabled) { + return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; + } + if (isHovering) { + return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; + } + return AppFlowyTheme.of(context).fillColorScheme.errorThick; + }, + ); + } + + /// Disabled text button. + factory AFFilledButton.disabled({ + Key? key, + required AFFilledButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFFilledButton._( + key: key, + builder: builder, + onTap: () {}, + size: size, + disabled: true, + padding: padding, + borderRadius: borderRadius, + backgroundColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, + ); + } + + final VoidCallback onTap; + final bool disabled; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFBaseButtonColorBuilder? backgroundColor; + final AFFilledButtonWidgetBuilder builder; + + @override + Widget build(BuildContext context) { + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: builder, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart new file mode 100644 index 0000000000..04c49d0b01 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart @@ -0,0 +1,199 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFFilledIconBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFFilledIconTextButton extends StatelessWidget { + const AFFilledIconTextButton._({ + super.key, + required this.text, + required this.onTap, + required this.iconBuilder, + this.textColor, + this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + }); + + /// Primary filled text button. + factory AFFilledIconTextButton.primary({ + Key? key, + required String text, + required VoidCallback onTap, + required AFFilledIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFFilledIconTextButton._( + key: key, + text: text, + onTap: onTap, + iconBuilder: iconBuilder, + size: size, + padding: padding, + borderRadius: borderRadius, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.tertiary; + } + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.fillColorScheme.themeThick; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.textColorScheme.onFill; + }, + ); + } + + /// Destructive filled text button. + factory AFFilledIconTextButton.destructive({ + Key? key, + required String text, + required VoidCallback onTap, + required AFFilledIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFFilledIconTextButton._( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.tertiary; + } + if (isHovering) { + return theme.fillColorScheme.errorThickHover; + } + return theme.fillColorScheme.errorThick; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.textColorScheme.onFill; + }, + ); + } + + /// Disabled filled text button. + factory AFFilledIconTextButton.disabled({ + Key? key, + required String text, + required AFFilledIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFFilledIconTextButton._( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.fillColorScheme.tertiary; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.textColorScheme.onFill; + }, + ); + } + + /// Ghost filled text button with transparent background that shows color on hover. + factory AFFilledIconTextButton.ghost({ + Key? key, + required String text, + required VoidCallback onTap, + required AFFilledIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFFilledIconTextButton._( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return Colors.transparent; + } + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return Colors.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + return theme.textColorScheme.primary; + }, + ); + } + + final String text; + final VoidCallback onTap; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFFilledIconBuilder iconBuilder; + + final AFBaseButtonColorBuilder? textColor; + final AFBaseButtonColorBuilder? backgroundColor; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return AFBaseButton( + backgroundColor: backgroundColor, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.onFill; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconBuilder(context, isHovering, disabled), + SizedBox(width: theme.spacing.s), + Text( + text, + style: size.buildTextStyle(context).copyWith( + color: textColor, + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart new file mode 100644 index 0000000000..353d5ac785 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -0,0 +1,141 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +class AFFilledTextButton extends AFBaseTextButton { + const AFFilledTextButton({ + super.key, + required super.text, + required super.onTap, + required super.backgroundColor, + required super.textColor, + super.size = AFButtonSize.m, + super.padding, + super.borderRadius, + super.disabled = false, + super.alignment, + }); + + /// Primary text button. + factory AFFilledTextButton.primary({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + }) { + return AFFilledTextButton( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + textColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).textColorScheme.onFill, + backgroundColor: (context, isHovering, disabled) { + if (disabled) { + return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; + } + if (isHovering) { + return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; + } + return AppFlowyTheme.of(context).fillColorScheme.themeThick; + }, + ); + } + + /// Destructive text button. + factory AFFilledTextButton.destructive({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + }) { + return AFFilledTextButton( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + textColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).textColorScheme.onFill, + backgroundColor: (context, isHovering, disabled) { + if (disabled) { + return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; + } + if (isHovering) { + return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; + } + return AppFlowyTheme.of(context).fillColorScheme.errorThick; + }, + ); + } + + /// Disabled text button. + factory AFFilledTextButton.disabled({ + Key? key, + required String text, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + Alignment? alignment, + }) { + return AFFilledTextButton( + key: key, + text: text, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + alignment: alignment, + textColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).textColorScheme.tertiary, + backgroundColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, + ); + } + + @override + Widget build(BuildContext context) { + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + AppFlowyTheme.of(context).textColorScheme.onFill; + Widget child = Text( + text, + style: size.buildTextStyle(context).copyWith(color: textColor), + ); + + final alignment = this.alignment; + if (alignment != null) { + child = Align( + alignment: alignment, + child: child, + ); + } + + return child; + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart new file mode 100644 index 0000000000..47ff96e878 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart @@ -0,0 +1,96 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFGhostButtonWidgetBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFGhostButton extends StatelessWidget { + const AFGhostButton._({ + super.key, + required this.onTap, + required this.backgroundColor, + required this.builder, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Normal ghost button. + factory AFGhostButton.normal({ + Key? key, + required VoidCallback onTap, + required AFGhostButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFGhostButton._( + key: key, + builder: builder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + ); + } + + /// Disabled ghost button. + factory AFGhostButton.disabled({ + Key? key, + required AFGhostButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFGhostButton._( + key: key, + builder: builder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + backgroundColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).fillColorScheme.transparent, + ); + } + + final VoidCallback onTap; + final bool disabled; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFBaseButtonColorBuilder? backgroundColor; + final AFGhostButtonWidgetBuilder builder; + + @override + Widget build(BuildContext context) { + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: builder, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart new file mode 100644 index 0000000000..e65eb2dd7e --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart @@ -0,0 +1,141 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFGhostIconBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFGhostIconTextButton extends StatelessWidget { + const AFGhostIconTextButton({ + super.key, + required this.text, + required this.onTap, + required this.iconBuilder, + this.textColor, + this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Primary ghost text button. + factory AFGhostIconTextButton.primary({ + Key? key, + required String text, + required VoidCallback onTap, + required AFGhostIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFGhostIconTextButton( + key: key, + text: text, + onTap: onTap, + iconBuilder: iconBuilder, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return Colors.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return Colors.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + return theme.textColorScheme.primary; + }, + ); + } + + /// Disabled ghost text button. + factory AFGhostIconTextButton.disabled({ + Key? key, + required String text, + required AFGhostIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFGhostIconTextButton( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + backgroundColor: (context, isHovering, disabled) { + return Colors.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.textColorScheme.tertiary; + }, + ); + } + + final String text; + final bool disabled; + final VoidCallback onTap; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFGhostIconBuilder iconBuilder; + + final AFBaseButtonColorBuilder? textColor; + final AFBaseButtonColorBuilder? backgroundColor; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (context, isHovering, disabled) { + return Colors.transparent; + }, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.primary; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconBuilder( + context, + isHovering, + disabled, + ), + SizedBox(width: theme.spacing.m), + Text( + text, + style: size.buildTextStyle(context).copyWith( + color: textColor, + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart new file mode 100644 index 0000000000..441b544f8a --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart @@ -0,0 +1,116 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +class AFGhostTextButton extends AFBaseTextButton { + const AFGhostTextButton({ + super.key, + required super.text, + required super.onTap, + super.textColor, + super.backgroundColor, + super.size = AFButtonSize.m, + super.padding, + super.borderRadius, + super.disabled = false, + super.alignment, + }); + + /// Normal ghost text button. + factory AFGhostTextButton.primary({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + }) { + return AFGhostTextButton( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + if (isHovering) { + return theme.textColorScheme.primary; + } + return theme.textColorScheme.primary; + }, + ); + } + + /// Disabled ghost text button. + factory AFGhostTextButton.disabled({ + Key? key, + required String text, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + Alignment? alignment, + }) { + return AFGhostTextButton( + key: key, + text: text, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + alignment: alignment, + textColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).textColorScheme.tertiary, + backgroundColor: (context, isHovering, disabled) => + AppFlowyTheme.of(context).fillColorScheme.transparent, + ); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.primary; + + Widget child = Text( + text, + style: size.buildTextStyle(context).copyWith(color: textColor), + ); + + final alignment = this.alignment; + if (alignment != null) { + child = Align( + alignment: alignment, + child: child, + ); + } + + return child; + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart new file mode 100644 index 0000000000..3b0ea7a06d --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart @@ -0,0 +1,168 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFOutlinedButtonWidgetBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFOutlinedButton extends StatelessWidget { + const AFOutlinedButton._({ + super.key, + required this.onTap, + required this.builder, + this.borderColor, + this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Normal outlined button. + factory AFOutlinedButton.normal({ + Key? key, + required AFOutlinedButtonWidgetBuilder builder, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFOutlinedButton._( + key: key, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + builder: builder, + ); + } + + /// Destructive outlined button. + factory AFOutlinedButton.destructive({ + Key? key, + required AFOutlinedButtonWidgetBuilder builder, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFOutlinedButton._( + key: key, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorThickHover; + } + return theme.fillColorScheme.errorThick; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorSelect; + } + return theme.fillColorScheme.transparent; + }, + builder: builder, + ); + } + + /// Disabled outlined text button. + factory AFOutlinedButton.disabled({ + Key? key, + required AFOutlinedButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFOutlinedButton._( + key: key, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + builder: builder, + ); + } + + final VoidCallback onTap; + final bool disabled; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonColorBuilder? backgroundColor; + + final AFOutlinedButtonWidgetBuilder builder; + + @override + Widget build(BuildContext context) { + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: borderColor, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: builder, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart new file mode 100644 index 0000000000..710a4ccca5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart @@ -0,0 +1,226 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFOutlinedIconBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFOutlinedIconTextButton extends StatelessWidget { + const AFOutlinedIconTextButton._({ + super.key, + required this.text, + required this.onTap, + required this.iconBuilder, + this.borderColor, + this.textColor, + this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + this.alignment = MainAxisAlignment.center, + }); + + /// Normal outlined text button. + factory AFOutlinedIconTextButton.normal({ + Key? key, + required String text, + required VoidCallback onTap, + required AFOutlinedIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + MainAxisAlignment alignment = MainAxisAlignment.center, + }) { + return AFOutlinedIconTextButton._( + key: key, + text: text, + onTap: onTap, + iconBuilder: iconBuilder, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + if (isHovering) { + return theme.textColorScheme.primary; + } + return theme.textColorScheme.primary; + }, + ); + } + + /// Destructive outlined text button. + factory AFOutlinedIconTextButton.destructive({ + Key? key, + required String text, + required VoidCallback onTap, + required AFOutlinedIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + MainAxisAlignment alignment = MainAxisAlignment.center, + }) { + return AFOutlinedIconTextButton._( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorThickHover; + } + return theme.fillColorScheme.errorThick; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorThickHover; + } + return theme.fillColorScheme.errorThick; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return disabled + ? theme.textColorScheme.error + : theme.textColorScheme.error; + }, + ); + } + + /// Disabled outlined text button. + factory AFOutlinedIconTextButton.disabled({ + Key? key, + required String text, + required AFOutlinedIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + MainAxisAlignment alignment = MainAxisAlignment.center, + }) { + return AFOutlinedIconTextButton._( + key: key, + text: text, + iconBuilder: iconBuilder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + alignment: alignment, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return disabled + ? theme.textColorScheme.tertiary + : theme.textColorScheme.primary; + }, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + ); + } + + final String text; + final bool disabled; + final VoidCallback onTap; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + final MainAxisAlignment alignment; + + final AFOutlinedIconBuilder iconBuilder; + + final AFBaseButtonColorBuilder? textColor; + final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonColorBuilder? backgroundColor; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return AFBaseButton( + backgroundColor: backgroundColor, + borderColor: borderColor, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + disabled: disabled, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.primary; + return Row( + mainAxisAlignment: alignment, + children: [ + iconBuilder(context, isHovering, disabled), + SizedBox(width: theme.spacing.s), + Text( + text, + style: size.buildTextStyle(context).copyWith( + color: textColor, + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart new file mode 100644 index 0000000000..7cb5f2d609 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -0,0 +1,204 @@ +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +class AFOutlinedTextButton extends AFBaseTextButton { + const AFOutlinedTextButton._({ + super.key, + required super.text, + required super.onTap, + this.borderColor, + super.textColor, + super.backgroundColor, + super.size = AFButtonSize.m, + super.padding, + super.borderRadius, + super.disabled = false, + super.alignment, + }); + + /// Normal outlined text button. + factory AFOutlinedTextButton.normal({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + }) { + return AFOutlinedTextButton._( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + if (isHovering) { + return theme.textColorScheme.primary; + } + return theme.textColorScheme.primary; + }, + ); + } + + /// Destructive outlined text button. + factory AFOutlinedTextButton.destructive({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + }) { + return AFOutlinedTextButton._( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorThickHover; + } + return theme.fillColorScheme.errorThick; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.errorThick; + } + if (isHovering) { + return theme.fillColorScheme.errorSelect; + } + return theme.fillColorScheme.transparent; + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return disabled + ? theme.textColorScheme.error + : theme.textColorScheme.error; + }, + ); + } + + /// Disabled outlined text button. + factory AFOutlinedTextButton.disabled({ + Key? key, + required String text, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + Alignment? alignment, + }) { + return AFOutlinedTextButton._( + key: key, + text: text, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + alignment: alignment, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return disabled + ? theme.textColorScheme.tertiary + : theme.textColorScheme.primary; + }, + borderColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.borderColorScheme.greyTertiary; + } + if (isHovering) { + return theme.borderColorScheme.greyTertiaryHover; + } + return theme.borderColorScheme.greyTertiary; + }, + backgroundColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.fillColorScheme.transparent; + } + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } + return theme.fillColorScheme.transparent; + }, + ); + } + + final AFBaseButtonColorBuilder? borderColor; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: borderColor, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.primary; + + Widget child = Text( + text, + style: size.buildTextStyle(context).copyWith(color: textColor), + ); + + final alignment = this.alignment; + + if (alignment != null) { + child = Align( + alignment: alignment, + child: child, + ); + } + + return child; + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart new file mode 100644 index 0000000000..d01d64109c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart @@ -0,0 +1,2 @@ +export 'button/button.dart'; +export 'textfield/textfield.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart new file mode 100644 index 0000000000..9fed31027f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -0,0 +1,177 @@ +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFTextFieldValidator = (bool result, String errorText) Function( + TextEditingController controller, +); + +class AFTextField extends StatefulWidget { + const AFTextField({ + super.key, + this.hintText, + this.initialText, + this.keyboardType, + this.radius, + this.validator, + this.controller, + this.onChanged, + this.onSubmitted, + this.autoFocus, + }); + + /// The hint text to display when the text field is empty. + final String? hintText; + + /// The initial text to display in the text field. + final String? initialText; + + /// The type of keyboard to display. + final TextInputType? keyboardType; + + /// The radius of the text field. + final double? radius; + + /// The validator to use for the text field. + final AFTextFieldValidator? validator; + + /// The controller to use for the text field. + /// + /// If it's not provided, the text field will use a new controller. + final TextEditingController? controller; + + /// The callback to call when the text field changes. + final void Function(String)? onChanged; + + /// The callback to call when the text field is submitted. + final void Function(String)? onSubmitted; + + /// Enable auto focus. + final bool? autoFocus; + + @override + State createState() => _AFTextFieldState(); +} + +class _AFTextFieldState extends State { + late final TextEditingController effectiveController; + + bool hasError = false; + String errorText = ''; + + @override + void initState() { + super.initState(); + + effectiveController = widget.controller ?? TextEditingController(); + + final initialText = widget.initialText; + if (initialText != null) { + effectiveController.text = initialText; + } + + effectiveController.addListener(_validate); + } + + @override + void dispose() { + effectiveController.removeListener(_validate); + if (widget.controller == null) { + effectiveController.dispose(); + } + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + final borderRadius = BorderRadius.circular( + widget.radius ?? theme.borderRadius.l, + ); + + final errorBorderColor = theme.borderColorScheme.errorThick; + final defaultBorderColor = theme.borderColorScheme.greyTertiary; + + Widget child = TextField( + controller: effectiveController, + keyboardType: widget.keyboardType, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + onChanged: widget.onChanged, + onSubmitted: widget.onSubmitted, + autofocus: widget.autoFocus ?? false, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.tertiary, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: theme.spacing.m, + vertical: 10, + ), + border: OutlineInputBorder( + borderSide: BorderSide( + color: hasError ? errorBorderColor : defaultBorderColor, + ), + borderRadius: borderRadius, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: hasError ? errorBorderColor : defaultBorderColor, + ), + borderRadius: borderRadius, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: hasError + ? errorBorderColor + : theme.borderColorScheme.themeThick, + ), + borderRadius: borderRadius, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorBorderColor, + ), + borderRadius: borderRadius, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorBorderColor, + ), + borderRadius: borderRadius, + ), + hoverColor: theme.borderColorScheme.greyTertiaryHover, + ), + ); + + if (hasError && errorText.isNotEmpty) { + child = Column( + children: [ + child, + SizedBox(height: theme.spacing.xs), + Text( + errorText, + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.error, + ), + ), + ], + ); + } + + return child; + } + + void _validate() { + final validator = widget.validator; + if (validator != null) { + final result = validator(effectiveController); + setState(() { + hasError = result.$1; + errorText = result.$2; + }); + } + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart new file mode 100644 index 0000000000..49deecc178 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -0,0 +1,72 @@ +import 'package:appflowy_ui/src/theme/data/data.dart'; +import 'package:flutter/widgets.dart'; + +class AppFlowyTheme extends StatelessWidget { + const AppFlowyTheme({ + super.key, + required this.data, + required this.child, + }); + + final AppFlowyThemeData data; + final Widget child; + + static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { + final provider = maybeOf(context, listen: listen); + if (provider == null) { + throw FlutterError( + ''' + AppFlowyTheme.of() called with a context that does not contain a AppFlowyTheme.\n + No AppFlowyTheme ancestor could be found starting from the context that was passed to AppFlowyTheme.of(). + This can happen because you do not have a AppFlowyTheme widget (which introduces a AppFlowyTheme), + or it can happen if the context you use comes from a widget above this widget.\n + The context used was: $context''', + ); + } + return provider; + } + + static AppFlowyThemeData? maybeOf( + BuildContext context, { + bool listen = true, + }) { + if (listen) { + return context + .dependOnInheritedWidgetOfExactType() + ?.theme + .data; + } + final provider = context + .getElementForInheritedWidgetOfExactType() + ?.widget; + + return (provider as AppFlowyInheritedTheme?)?.theme.data; + } + + @override + Widget build(BuildContext context) { + return AppFlowyInheritedTheme( + theme: this, + child: child, + ); + } +} + +class AppFlowyInheritedTheme extends InheritedTheme { + const AppFlowyInheritedTheme({ + super.key, + required this.theme, + required super.child, + }); + + final AppFlowyTheme theme; + + @override + Widget wrap(BuildContext context, Widget child) { + return AppFlowyTheme(data: theme.data, child: child); + } + + @override + bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => + theme.data != oldWidget.theme.data; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart new file mode 100644 index 0000000000..fb07a5fe64 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart @@ -0,0 +1,17 @@ +class AppFlowyBorderRadius { + const AppFlowyBorderRadius({ + required this.xs, + required this.s, + required this.m, + required this.l, + required this.xl, + required this.xxl, + }); + + final double xs; + final double s; + final double m; + final double l; + final double xl; + final double xxl; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart new file mode 100644 index 0000000000..547c7f1635 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class AppFlowyBackgroundColorScheme { + const AppFlowyBackgroundColorScheme({ + required this.primary, + required this.secondary, + required this.tertiary, + required this.quaternary, + }); + + final Color primary; + final Color secondary; + final Color tertiary; + final Color quaternary; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart new file mode 100644 index 0000000000..5b843d97e2 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart @@ -0,0 +1,33 @@ +import 'package:appflowy_ui/src/theme/color_scheme/base/blue.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/green.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/magenta.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/neutral.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/orange.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/purple.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/red.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/subtle.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/yellow.dart'; + +class AppFlowyBaseColorScheme { + const AppFlowyBaseColorScheme({ + this.blue = const BlueColors(), + this.green = const GreenColors(), + this.yellow = const YellowColors(), + this.red = const RedColors(), + this.orange = const OrangeColors(), + this.magenta = const MagentaColors(), + this.purple = const PurpleColors(), + this.neutral = const NeutralColors(), + this.subtle = const SubtleColors(), + }); + + final BlueColors blue; + final GreenColors green; + final YellowColors yellow; + final RedColors red; + final OrangeColors orange; + final MagentaColors magenta; + final PurpleColors purple; + final NeutralColors neutral; + final SubtleColors subtle; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart new file mode 100644 index 0000000000..fb73b42f60 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class BlueColors { + const BlueColors(); + + Color get blue100 => const Color(0xFFE3F6FF); + + Color get blue200 => const Color(0xFFA9E2FF); + + Color get blue300 => const Color(0xFF80D2FF); + + Color get blue400 => const Color(0xFF4EC1FF); + + Color get blue500 => const Color(0xFF00B5FF); + + Color get blue600 => const Color(0xFF0092D6); + + Color get blue700 => const Color(0xFF0078C0); + + Color get blue800 => const Color(0xFF0065A9); + + Color get blue900 => const Color(0xFF00508F); + + Color get blue1000 => const Color(0xFF003C77); + + Color get alphaBlue50015 => const Color(0x2600B5FF); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart new file mode 100644 index 0000000000..652f5f5932 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class GreenColors { + const GreenColors(); + Color get green100 => const Color(0xFFECF9F5); + + Color get green200 => const Color(0xFFC3E5D8); + + Color get green300 => const Color(0xFF9AD1BC); + + Color get green400 => const Color(0xFF71BD9F); + + Color get green500 => const Color(0xFF48A982); + + Color get green600 => const Color(0xFF248569); + + Color get green700 => const Color(0xFF29725D); + + Color get green800 => const Color(0xFF2E6050); + + Color get green900 => const Color(0xFF305548); + + Color get green1000 => const Color(0xFF305244); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart new file mode 100644 index 0000000000..dec5617c67 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class MagentaColors { + const MagentaColors(); + + Color get magenta100 => const Color(0xFFFFE5EF); + + Color get magenta200 => const Color(0xFFFFB8D1); + + Color get magenta300 => const Color(0xFFFF8AB2); + + Color get magenta400 => const Color(0xFFFF5C93); + + Color get magenta500 => const Color(0xFFFB006D); + + Color get magenta600 => const Color(0xFFD2005F); + + Color get magenta700 => const Color(0xFFD2005F); + + Color get magenta800 => const Color(0xFF850040); + + Color get magenta900 => const Color(0xFF610031); + + Color get magenta1000 => const Color(0xFF400022); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart new file mode 100644 index 0000000000..4b6c08b595 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class NeutralColors { + const NeutralColors(); + + Color get neutral100 => const Color(0xFFF8FAFF); + + Color get neutral200 => const Color(0xFFE4E8F5); + + Color get neutral300 => const Color(0xFFCED3E6); + + Color get neutral400 => const Color(0xFFB5BBD3); + + Color get neutral500 => const Color(0xFF989EB7); + + Color get neutral600 => const Color(0xFF6F748C); + + Color get neutral700 => const Color(0xFF54596E); + + Color get neutral800 => const Color(0xFF3C3F4E); + + Color get neutral900 => const Color(0xFF272930); + + Color get neutral1000 => const Color(0xFF21232A); + + Color get black => const Color(0xFF000000); + + Color get alphaBlack60 => const Color(0x99000000); + + Color get white => const Color(0xFFFFFFFF); + + Color get alphaWhite0 => const Color(0x00FFFFFF); + + Color get alphaWhite20 => const Color(0x33FFFFFF); + + Color get alphaWhite30 => const Color(0x4DFFFFFF); + + Color get alphaGrey10005 => const Color(0x0DF9FAFD); + + Color get alphaGrey10010 => const Color(0x1AF9FAFD); + + Color get alphaGrey100005 => const Color(0x0D1F2329); + + Color get alphaGrey100010 => const Color(0x1A1F2329); + + Color get alphaGrey100070 => const Color(0xB21F2329); + + Color get alphaGrey100080 => const Color(0xCC1F2329); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart new file mode 100644 index 0000000000..c9424bd960 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class OrangeColors { + const OrangeColors(); + + Color get orange100 => const Color(0xFFFFF3D5); + + Color get orange200 => const Color(0xFFFFE4AB); + + Color get orange300 => const Color(0xFFFFD181); + + Color get orange400 => const Color(0xFFFFBE62); + + Color get orange500 => const Color(0xFFFFA02E); + + Color get orange600 => const Color(0xFFDB7E21); + + Color get orange700 => const Color(0xFFB75F17); + + Color get orange800 => const Color(0xFF93450E); + + Color get orange900 => const Color(0xFF7A3108); + + Color get orange1000 => const Color(0xFF602706); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart new file mode 100644 index 0000000000..fa3b9e54cf --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class PurpleColors { + const PurpleColors(); + + Color get purple100 => const Color(0xFFF1E0FF); + + Color get purple200 => const Color(0xFFE1B3FF); + + Color get purple300 => const Color(0xFFD185FF); + + Color get purple400 => const Color(0xFFBC58FF); + + Color get purple500 => const Color(0xFF9327FF); + + Color get purple600 => const Color(0xFF7A1DCC); + + Color get purple700 => const Color(0xFF6617B3); + + Color get purple800 => const Color(0xFF55138F); + + Color get purple900 => const Color(0xFF470C72); + + Color get purple1000 => const Color(0xFF380758); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart new file mode 100644 index 0000000000..030f2988bc --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class RedColors { + const RedColors(); + + Color get red100 => const Color(0xFFFFD2DD); + + Color get red200 => const Color(0xFFFFA5B4); + + Color get red300 => const Color(0xFFFF7D87); + + Color get red400 => const Color(0xFFFF5050); + + Color get red500 => const Color(0xFFF33641); + + Color get red600 => const Color(0xFFE71D32); + + Color get red700 => const Color(0xFFAD1625); + + Color get red800 => const Color(0xFF8C101C); + + Color get red900 => const Color(0xFF6E0A1E); + + Color get red1000 => const Color(0xFF4C0A17); + + Color get alphaRed50010 => const Color(0x1AF33641); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart new file mode 100644 index 0000000000..2fddd14f1a --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; + +class SubtleColors { + const SubtleColors(); + + // Rose colors + Color get rose100 => const Color(0xFFFCF2F2); + + Color get rose200 => const Color(0xFFFAE3E3); + + Color get rose300 => const Color(0xFFFAD9D9); + + Color get rose400 => const Color(0xFFEDADAD); + + Color get rose500 => const Color(0xFFCC4E4E); + + Color get rose600 => const Color(0xFF702828); + + // Papaya colors + Color get papaya100 => const Color(0xFFFCF4F0); + + Color get papaya200 => const Color(0xFFFAE8DE); + + Color get papaya300 => const Color(0xFFFADFD2); + + Color get papaya400 => const Color(0xFFF0BDA3); + + Color get papaya500 => const Color(0xFFD67240); + + Color get papaya600 => const Color(0xFF6B3215); + + // Tangerine colors + Color get tangerine100 => const Color(0xFFFFF7ED); + + Color get tangerine200 => const Color(0xFFFCEDD9); + + Color get tangerine300 => const Color(0xFFFAE5CA); + + Color get tangerine400 => const Color(0xFFF2CB99); + + Color get tangerine500 => const Color(0xFFDB8F2C); + + Color get tangerine600 => const Color(0xFF613B0A); + + // Mango colors + Color get mango100 => const Color(0xFFFFF9EC); + + Color get mango200 => const Color(0xFFFCF1D7); + + Color get mango300 => const Color(0xFFFAE9C3); + + Color get mango400 => const Color(0xFFF5D68E); + + Color get mango500 => const Color(0xFFE0A416); + + Color get mango600 => const Color(0xFF5C4102); + + // Lemon colors + Color get lemon100 => const Color(0xFFFFFBE8); + + Color get lemon200 => const Color(0xFFFCF5CF); + + Color get lemon300 => const Color(0xFFFAEFB9); + + Color get lemon400 => const Color(0xFFF5E282); + + Color get lemon500 => const Color(0xFFE0BB00); + + Color get lemon600 => const Color(0xFF574800); + + // Olive colors + Color get olive100 => const Color(0xFFF9FAE6); + + Color get olive200 => const Color(0xFFF6F7D0); + + Color get olive300 => const Color(0xFFF0F2B3); + + Color get olive400 => const Color(0xFFDBDE83); + + Color get olive500 => const Color(0xFFADB204); + + Color get olive600 => const Color(0xFF4A4C03); + + // Lime colors + Color get lime100 => const Color(0xFFF6F9E6); + + Color get lime200 => const Color(0xFFEEF5CE); + + Color get lime300 => const Color(0xFFE7F0BB); + + Color get lime400 => const Color(0xFFCFDB91); + + Color get lime500 => const Color(0xFF92A822); + + Color get lime600 => const Color(0xFF414D05); + + // Grass colors + Color get grass100 => const Color(0xFFF4FAEB); + + Color get grass200 => const Color(0xFFE9F5D7); + + Color get grass300 => const Color(0xFFDEF0C5); + + Color get grass400 => const Color(0xFFBFD998); + + Color get grass500 => const Color(0xFF75A828); + + Color get grass600 => const Color(0xFF334D0C); + + // Forest colors + Color get forest100 => const Color(0xFFF1FAF0); + + Color get forest200 => const Color(0xFFE2F5DF); + + Color get forest300 => const Color(0xFFD7F0D3); + + Color get forest400 => const Color(0xFFA8D6A1); + + Color get forest500 => const Color(0xFF49A33B); + + Color get forest600 => const Color(0xFF1E4F16); + + // Jade colors + Color get jade100 => const Color(0xFFF0FAF6); + + Color get jade200 => const Color(0xFFDFF5EB); + + Color get jade300 => const Color(0xFFCEF0E1); + + Color get jade400 => const Color(0xFF90D1B5); + + Color get jade500 => const Color(0xFF1C9963); + + Color get jade600 => const Color(0xFF075231); + + // Aqua colors + Color get aqua100 => const Color(0xFFF0F9FA); + + Color get aqua200 => const Color(0xFFDFF3F5); + + Color get aqua300 => const Color(0xFFCCECF0); + + Color get aqua400 => const Color(0xFF83CCD4); + + Color get aqua500 => const Color(0xFF008E9E); + + Color get aqua600 => const Color(0xFF004E57); + + // Azure colors + Color get azure100 => const Color(0xFFF0F6FA); + + Color get azure200 => const Color(0xFFE1EEF7); + + Color get azure300 => const Color(0xFFD3E6F5); + + Color get azure400 => const Color(0xFF88C0EB); + + Color get azure500 => const Color(0xFF0877CC); + + Color get azure600 => const Color(0xFF154469); + + // Denim colors + Color get denim100 => const Color(0xFFF0F3FA); + + Color get denim200 => const Color(0xFFE3EBFA); + + Color get denim300 => const Color(0xFFD7E2F7); + + Color get denim400 => const Color(0xFF9AB6ED); + + Color get denim500 => const Color(0xFF3267D1); + + Color get denim600 => const Color(0xFF223C70); + + // Mauve colors + Color get mauve100 => const Color(0xFFF2F2FC); + + Color get mauve200 => const Color(0xFFE6E6FA); + + Color get mauve300 => const Color(0xFFDCDCF7); + + Color get mauve400 => const Color(0xFFAEAEF5); + + Color get mauve500 => const Color(0xFF5555E0); + + Color get mauve600 => const Color(0xFF36366B); + + // Lavender colors + Color get lavender100 => const Color(0xFFF6F3FC); + + Color get lavender200 => const Color(0xFFEBE3FA); + + Color get lavender300 => const Color(0xFFE4DAF7); + + Color get lavender400 => const Color(0xFFC1AAF0); + + Color get lavender500 => const Color(0xFF8153DB); + + Color get lavender600 => const Color(0xFF462F75); + + // Lilac colors + Color get lilac100 => const Color(0xFFF7F0FA); + + Color get lilac200 => const Color(0xFFF0E1F7); + + Color get lilac300 => const Color(0xFFEDD7F7); + + Color get lilac400 => const Color(0xFFD3A9E8); + + Color get lilac500 => const Color(0xFF9E4CC7); + + Color get lilac600 => const Color(0xFF562D6B); + + // Mallow colors + Color get mallow100 => const Color(0xFFFAF0FA); + + Color get mallow200 => const Color(0xFFF5E1F4); + + Color get mallow300 => const Color(0xFFF5D7F4); + + Color get mallow400 => const Color(0xFFDEA4DC); + + Color get mallow500 => const Color(0xFFB240AF); + + Color get mallow600 => const Color(0xFF632861); + + // Camellia colors + Color get camellia100 => const Color(0xFFF9EFF3); + + Color get camellia200 => const Color(0xFFF7E1EB); + + Color get camellia300 => const Color(0xFFF7D7E5); + + Color get camellia400 => const Color(0xFFE5A3C0); + + Color get camellia500 => const Color(0xFFC24279); + + Color get camellia600 => const Color(0xFF6E2343); + + // Smoke colors + Color get smoke100 => const Color(0xFFF5F5F5); + + Color get smoke200 => const Color(0xFFE8E8E8); + + Color get smoke300 => const Color(0xFFDEDEDE); + + Color get smoke400 => const Color(0xFFB8B8B8); + + Color get smoke500 => const Color(0xFF6E6E6E); + + Color get smoke600 => const Color(0xFF404040); + + // Iron colors + Color get iron100 => const Color(0xFFF2F4F7); + + Color get iron200 => const Color(0xFFE6E9F0); + + Color get iron300 => const Color(0xFFDADEE5); + + Color get iron400 => const Color(0xFFB0B5BF); + + Color get iron500 => const Color(0xFF666F80); + + Color get iron600 => const Color(0xFF394152); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart new file mode 100644 index 0000000000..a38cf2bd78 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class YellowColors { + const YellowColors(); + Color get yellow100 => const Color(0xFFFFF9B2); + + Color get yellow200 => const Color(0xFFFFEC66); + + Color get yellow300 => const Color(0xFFFFDF1A); + + Color get yellow400 => const Color(0xFFFFCC00); + + Color get yellow500 => const Color(0xFFFFCE00); + + Color get yellow600 => const Color(0xFFE6B800); + + Color get yellow700 => const Color(0xFFCC9F00); + + Color get yellow800 => const Color(0xFFB38A00); + + Color get yellow900 => const Color(0xFF9A7500); + + Color get yellow1000 => const Color(0xFF7F6200); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart new file mode 100644 index 0000000000..d1618b6cff --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class AppFlowyBorderColorScheme { + const AppFlowyBorderColorScheme({ + required this.greyPrimary, + required this.greyPrimaryHover, + required this.greySecondary, + required this.greySecondaryHover, + required this.greyTertiary, + required this.greyTertiaryHover, + required this.greyQuaternary, + required this.greyQuaternaryHover, + required this.transparent, + required this.themeThick, + required this.themeThickHover, + required this.infoThick, + required this.infoThickHover, + required this.successThick, + required this.successThickHover, + required this.warningThick, + required this.warningThickHover, + required this.errorThick, + required this.errorThickHover, + required this.purpleThick, + required this.purpleThickHover, + }); + + final Color greyPrimary; + final Color greyPrimaryHover; + final Color greySecondary; + final Color greySecondaryHover; + final Color greyTertiary; + final Color greyTertiaryHover; + final Color greyQuaternary; + final Color greyQuaternaryHover; + final Color transparent; + final Color themeThick; + final Color themeThickHover; + final Color infoThick; + final Color infoThickHover; + final Color successThick; + final Color successThickHover; + final Color warningThick; + final Color warningThickHover; + final Color errorThick; + final Color errorThickHover; + final Color purpleThick; + final Color purpleThickHover; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart new file mode 100644 index 0000000000..d1618b6cff --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class AppFlowyBorderColorScheme { + const AppFlowyBorderColorScheme({ + required this.greyPrimary, + required this.greyPrimaryHover, + required this.greySecondary, + required this.greySecondaryHover, + required this.greyTertiary, + required this.greyTertiaryHover, + required this.greyQuaternary, + required this.greyQuaternaryHover, + required this.transparent, + required this.themeThick, + required this.themeThickHover, + required this.infoThick, + required this.infoThickHover, + required this.successThick, + required this.successThickHover, + required this.warningThick, + required this.warningThickHover, + required this.errorThick, + required this.errorThickHover, + required this.purpleThick, + required this.purpleThickHover, + }); + + final Color greyPrimary; + final Color greyPrimaryHover; + final Color greySecondary; + final Color greySecondaryHover; + final Color greyTertiary; + final Color greyTertiaryHover; + final Color greyQuaternary; + final Color greyQuaternaryHover; + final Color transparent; + final Color themeThick; + final Color themeThickHover; + final Color infoThick; + final Color infoThickHover; + final Color successThick; + final Color successThickHover; + final Color warningThick; + final Color warningThickHover; + final Color errorThick; + final Color errorThickHover; + final Color purpleThick; + final Color purpleThickHover; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart new file mode 100644 index 0000000000..8374d2bbfd --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class AppFlowyBrandColorScheme { + const AppFlowyBrandColorScheme({ + required this.skyline, + required this.aqua, + required this.violet, + required this.amethyst, + required this.berry, + required this.coral, + required this.golden, + required this.amber, + required this.lemon, + }); + + final Color skyline; + final Color aqua; + final Color violet; + final Color amethyst; + final Color berry; + final Color coral; + final Color golden; + final Color amber; + final Color lemon; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart new file mode 100644 index 0000000000..5a1e7debeb --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart @@ -0,0 +1,8 @@ +export 'background/background_color_scheme.dart'; +export 'base/base_scheme.dart'; +export 'border/border_color_scheme.dart'; +export 'brand/brand_color_scheme.dart'; +export 'fill/fill_color_scheme.dart'; +export 'icon/icon_color_theme.dart'; +export 'surface/surface_color_scheme.dart'; +export 'text/text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart new file mode 100644 index 0000000000..8616bde4eb --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +class AppFlowyFillColorScheme { + const AppFlowyFillColorScheme({ + required this.primary, + required this.primaryHover, + required this.secondary, + required this.secondaryHover, + required this.tertiary, + required this.tertiaryHover, + required this.quaternary, + required this.quaternaryHover, + required this.transparent, + required this.primaryAlpha5, + required this.primaryAlpha5Hover, + required this.primaryAlpha80, + required this.primaryAlpha80Hover, + required this.white, + required this.whiteAlpha, + required this.whiteAlphaHover, + required this.black, + required this.themeLight, + required this.themeLightHover, + required this.themeThick, + required this.themeThickHover, + required this.themeSelect, + required this.infoLight, + required this.infoLightHover, + required this.infoThick, + required this.infoThickHover, + required this.successLight, + required this.successLightHover, + required this.successThick, + required this.successThickHover, + required this.warningLight, + required this.warningLightHover, + required this.warningThick, + required this.warningThickHover, + required this.errorLight, + required this.errorLightHover, + required this.errorThick, + required this.errorThickHover, + required this.errorSelect, + required this.purpleLight, + required this.purpleLightHover, + required this.purpleThick, + required this.purpleThickHover, + }); + + final Color primary; + final Color primaryHover; + final Color secondary; + final Color secondaryHover; + final Color tertiary; + final Color tertiaryHover; + final Color quaternary; + final Color quaternaryHover; + final Color transparent; + final Color primaryAlpha5; + final Color primaryAlpha5Hover; + final Color primaryAlpha80; + final Color primaryAlpha80Hover; + final Color white; + final Color whiteAlpha; + final Color whiteAlphaHover; + final Color black; + final Color themeLight; + final Color themeLightHover; + final Color themeThick; + final Color themeThickHover; + final Color themeSelect; + final Color infoLight; + final Color infoLightHover; + final Color infoThick; + final Color infoThickHover; + final Color successLight; + final Color successLightHover; + final Color successThick; + final Color successThickHover; + final Color warningLight; + final Color warningLightHover; + final Color warningThick; + final Color warningThickHover; + final Color errorLight; + final Color errorLightHover; + final Color errorThick; + final Color errorThickHover; + final Color errorSelect; + final Color purpleLight; + final Color purpleLightHover; + final Color purpleThick; + final Color purpleThickHover; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart new file mode 100644 index 0000000000..f9ece5339c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class AppFlowyIconColorTheme { + const AppFlowyIconColorTheme({ + required this.primary, + required this.secondary, + required this.tertiary, + required this.quaternary, + required this.white, + required this.purpleThick, + required this.purpleThickHover, + }); + + final Color primary; + final Color secondary; + final Color tertiary; + final Color quaternary; + final Color white; + final Color purpleThick; + final Color purpleThickHover; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart new file mode 100644 index 0000000000..8fdc21adef --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class AppFlowySurfaceColorScheme { + const AppFlowySurfaceColorScheme({ + required this.primary, + required this.overlay, + }); + + final Color primary; + final Color overlay; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart new file mode 100644 index 0000000000..486378643f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class AppFlowyTextColorScheme { + const AppFlowyTextColorScheme({ + required this.primary, + required this.secondary, + required this.tertiary, + required this.quaternary, + required this.inverse, + required this.onFill, + required this.theme, + required this.themeHover, + required this.action, + required this.actionHover, + required this.info, + required this.infoHover, + required this.success, + required this.successHover, + required this.warning, + required this.warningHover, + required this.error, + required this.errorHover, + required this.purple, + required this.purpleHover, + }); + + final Color primary; + final Color secondary; + final Color tertiary; + final Color quaternary; + final Color inverse; + final Color onFill; + final Color theme; + final Color themeHover; + final Color action; + final Color actionHover; + final Color info; + final Color infoHover; + final Color success; + final Color successHover; + final Color warning; + final Color warningHover; + final Color error; + final Color errorHover; + final Color purple; + final Color purpleHover; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart new file mode 100644 index 0000000000..668644f196 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -0,0 +1,324 @@ +import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/dimensions.dart'; +import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; +import 'package:flutter/material.dart'; + +class AppFlowyThemeBuilder { + const AppFlowyThemeBuilder(); + + AppFlowyTextColorScheme buildTextColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyTextColorScheme( + primary: colorScheme.neutral.neutral1000, + secondary: colorScheme.neutral.neutral600, + tertiary: colorScheme.neutral.neutral400, + quaternary: colorScheme.neutral.neutral200, + inverse: colorScheme.neutral.white, + onFill: colorScheme.neutral.white, + theme: colorScheme.blue.blue500, + themeHover: colorScheme.blue.blue600, + action: colorScheme.blue.blue500, + actionHover: colorScheme.blue.blue600, + info: colorScheme.blue.blue500, + infoHover: colorScheme.blue.blue600, + success: colorScheme.green.green600, + successHover: colorScheme.green.green700, + warning: colorScheme.orange.orange600, + warningHover: colorScheme.orange.orange700, + error: colorScheme.red.red600, + errorHover: colorScheme.red.red700, + purple: colorScheme.purple.purple500, + purpleHover: colorScheme.purple.purple600, + ), + Brightness.dark => AppFlowyTextColorScheme( + primary: colorScheme.neutral.neutral200, + secondary: colorScheme.neutral.neutral400, + tertiary: colorScheme.neutral.neutral600, + quaternary: colorScheme.neutral.neutral1000, + inverse: colorScheme.neutral.neutral1000, + onFill: colorScheme.neutral.white, + theme: colorScheme.blue.blue500, + themeHover: colorScheme.blue.blue600, + action: colorScheme.blue.blue500, + actionHover: colorScheme.blue.blue600, + info: colorScheme.blue.blue500, + infoHover: colorScheme.blue.blue600, + success: colorScheme.green.green600, + successHover: colorScheme.green.green700, + warning: colorScheme.orange.orange600, + warningHover: colorScheme.orange.orange700, + error: colorScheme.red.red500, + errorHover: colorScheme.red.red400, + purple: colorScheme.purple.purple500, + purpleHover: colorScheme.purple.purple600, + ), + }; + } + + AppFlowyIconColorTheme buildIconColorTheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyIconColorTheme( + primary: colorScheme.neutral.neutral1000, + secondary: colorScheme.neutral.neutral600, + tertiary: colorScheme.neutral.neutral400, + quaternary: colorScheme.neutral.neutral200, + white: colorScheme.neutral.white, + purpleThick: colorScheme.purple.purple500, + purpleThickHover: colorScheme.purple.purple600, + ), + Brightness.dark => AppFlowyIconColorTheme( + primary: colorScheme.neutral.neutral200, + secondary: colorScheme.neutral.neutral400, + tertiary: colorScheme.neutral.neutral600, + quaternary: colorScheme.neutral.neutral1000, + white: colorScheme.neutral.white, + purpleThick: const Color(0xFFFFFFFF), + purpleThickHover: const Color(0xFFFFFFFF), + ), + }; + } + + AppFlowyBorderColorScheme buildBorderColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyBorderColorScheme( + greyPrimary: colorScheme.neutral.neutral1000, + greyPrimaryHover: colorScheme.neutral.neutral900, + greySecondary: colorScheme.neutral.neutral800, + greySecondaryHover: colorScheme.neutral.neutral700, + greyTertiary: colorScheme.neutral.neutral300, + greyTertiaryHover: colorScheme.neutral.neutral400, + greyQuaternary: colorScheme.neutral.neutral100, + greyQuaternaryHover: colorScheme.neutral.neutral200, + transparent: colorScheme.neutral.alphaWhite0, + themeThick: colorScheme.blue.blue500, + themeThickHover: colorScheme.blue.blue600, + infoThick: colorScheme.blue.blue500, + infoThickHover: colorScheme.blue.blue600, + successThick: colorScheme.green.green600, + successThickHover: colorScheme.green.green700, + warningThick: colorScheme.orange.orange600, + warningThickHover: colorScheme.orange.orange700, + errorThick: colorScheme.red.red600, + errorThickHover: colorScheme.red.red700, + purpleThick: colorScheme.purple.purple500, + purpleThickHover: colorScheme.purple.purple600, + ), + Brightness.dark => AppFlowyBorderColorScheme( + greyPrimary: colorScheme.neutral.neutral100, + greyPrimaryHover: colorScheme.neutral.neutral200, + greySecondary: colorScheme.neutral.neutral300, + greySecondaryHover: colorScheme.neutral.neutral400, + greyTertiary: colorScheme.neutral.neutral800, + greyTertiaryHover: colorScheme.neutral.neutral700, + greyQuaternary: colorScheme.neutral.neutral1000, + greyQuaternaryHover: colorScheme.neutral.neutral900, + transparent: colorScheme.neutral.alphaWhite0, + themeThick: colorScheme.blue.blue500, + themeThickHover: colorScheme.blue.blue600, + infoThick: colorScheme.blue.blue500, + infoThickHover: colorScheme.blue.blue600, + successThick: colorScheme.green.green600, + successThickHover: colorScheme.green.green700, + warningThick: colorScheme.orange.orange600, + warningThickHover: colorScheme.orange.orange700, + errorThick: colorScheme.red.red500, + errorThickHover: colorScheme.red.red400, + purpleThick: colorScheme.purple.purple500, + purpleThickHover: colorScheme.purple.purple600, + ), + }; + } + + AppFlowyFillColorScheme buildFillColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyFillColorScheme( + primary: colorScheme.neutral.neutral100, + primaryHover: colorScheme.neutral.neutral200, + secondary: colorScheme.neutral.neutral300, + secondaryHover: colorScheme.neutral.neutral400, + tertiary: colorScheme.neutral.neutral600, + tertiaryHover: colorScheme.neutral.neutral500, + quaternary: colorScheme.neutral.neutral1000, + quaternaryHover: colorScheme.neutral.neutral900, + transparent: colorScheme.neutral.alphaWhite0, + primaryAlpha5: colorScheme.neutral.alphaGrey100005, + primaryAlpha5Hover: colorScheme.neutral.alphaGrey10010, + primaryAlpha80: colorScheme.neutral.alphaGrey100080, + primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, + white: colorScheme.neutral.white, + whiteAlpha: colorScheme.neutral.alphaWhite20, + whiteAlphaHover: colorScheme.neutral.alphaWhite30, + black: colorScheme.neutral.black, + themeLight: colorScheme.blue.blue100, + themeLightHover: colorScheme.blue.blue200, + themeThick: colorScheme.blue.blue500, + themeThickHover: colorScheme.blue.blue600, + themeSelect: colorScheme.blue.alphaBlue50015, + infoLight: colorScheme.blue.blue100, + infoLightHover: colorScheme.blue.blue200, + infoThick: colorScheme.blue.blue500, + infoThickHover: colorScheme.blue.blue600, + successLight: colorScheme.green.green100, + successLightHover: colorScheme.green.green200, + successThick: colorScheme.green.green600, + successThickHover: colorScheme.green.green700, + warningLight: colorScheme.orange.orange100, + warningLightHover: colorScheme.orange.orange200, + warningThick: colorScheme.orange.orange600, + warningThickHover: colorScheme.orange.orange700, + errorLight: colorScheme.red.red100, + errorLightHover: colorScheme.red.red200, + errorThick: colorScheme.red.red600, + errorThickHover: colorScheme.red.red700, + errorSelect: colorScheme.red.alphaRed50010, + purpleLight: colorScheme.purple.purple100, + purpleLightHover: colorScheme.purple.purple200, + purpleThick: colorScheme.purple.purple500, + purpleThickHover: colorScheme.purple.purple600, + ), + Brightness.dark => AppFlowyFillColorScheme( + primary: colorScheme.neutral.neutral1000, + primaryHover: colorScheme.neutral.neutral900, + secondary: colorScheme.neutral.neutral600, + secondaryHover: colorScheme.neutral.neutral500, + tertiary: colorScheme.neutral.neutral300, + tertiaryHover: colorScheme.neutral.neutral400, + quaternary: colorScheme.neutral.neutral100, + quaternaryHover: colorScheme.neutral.neutral200, + transparent: colorScheme.neutral.alphaWhite0, + primaryAlpha5: colorScheme.neutral.alphaGrey100005, + primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010, + primaryAlpha80: colorScheme.neutral.alphaGrey100080, + primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, + white: colorScheme.neutral.white, + whiteAlpha: colorScheme.neutral.alphaWhite20, + whiteAlphaHover: colorScheme.neutral.alphaWhite30, + black: colorScheme.neutral.black, + themeLight: colorScheme.blue.blue100, + themeLightHover: colorScheme.blue.blue200, + themeThick: colorScheme.blue.blue500, + themeThickHover: colorScheme.blue.blue600, + themeSelect: colorScheme.blue.alphaBlue50015, + infoLight: colorScheme.blue.blue100, + infoLightHover: colorScheme.blue.blue200, + infoThick: colorScheme.blue.blue500, + infoThickHover: colorScheme.blue.blue600, + successLight: colorScheme.green.green100, + successLightHover: colorScheme.green.green200, + successThick: colorScheme.green.green600, + successThickHover: colorScheme.green.green700, + warningLight: colorScheme.orange.orange100, + warningLightHover: colorScheme.orange.orange200, + warningThick: colorScheme.orange.orange600, + warningThickHover: colorScheme.orange.orange700, + errorLight: colorScheme.red.red100, + errorLightHover: colorScheme.red.red200, + errorThick: colorScheme.red.red600, + errorThickHover: colorScheme.red.red700, + errorSelect: colorScheme.red.alphaRed50010, + purpleLight: colorScheme.purple.purple100, + purpleLightHover: colorScheme.purple.purple200, + purpleThick: colorScheme.purple.purple500, + purpleThickHover: colorScheme.purple.purple600, + ), + }; + } + + AppFlowySurfaceColorScheme buildSurfaceColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowySurfaceColorScheme( + primary: colorScheme.neutral.white, + overlay: colorScheme.neutral.alphaBlack60, + ), + Brightness.dark => AppFlowySurfaceColorScheme( + primary: colorScheme.neutral.neutral900, + overlay: colorScheme.neutral.alphaBlack60, + ), + }; + } + + AppFlowyBackgroundColorScheme buildBackgroundColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyBackgroundColorScheme( + primary: colorScheme.neutral.white, + secondary: colorScheme.neutral.neutral100, + tertiary: colorScheme.neutral.neutral200, + quaternary: colorScheme.neutral.neutral300, + ), + Brightness.dark => AppFlowyBackgroundColorScheme( + primary: colorScheme.neutral.neutral1000, + secondary: colorScheme.neutral.neutral900, + tertiary: colorScheme.neutral.neutral800, + quaternary: colorScheme.neutral.neutral700, + ), + }; + } + + AppFlowyBrandColorScheme buildBrandColorScheme( + AppFlowyBaseColorScheme colorScheme, + ) { + return AppFlowyBrandColorScheme( + skyline: const Color(0xFF00B5FF), + aqua: const Color(0xFF00C8FF), + violet: const Color(0xFF9327FF), + amethyst: const Color(0xFF8427E0), + berry: const Color(0xFFE3006D), + coral: const Color(0xFFFB006D), + golden: const Color(0xFFF7931E), + amber: const Color(0xFFFFBD00), + lemon: const Color(0xFFFFCE00), + ); + } + + AppFlowyBorderRadius buildBorderRadius( + AppFlowyBaseColorScheme colorScheme, + ) { + return AppFlowyBorderRadius( + xs: AppFlowyBorderRadiusConstant.radius100, + s: AppFlowyBorderRadiusConstant.radius200, + m: AppFlowyBorderRadiusConstant.radius300, + l: AppFlowyBorderRadiusConstant.radius400, + xl: AppFlowyBorderRadiusConstant.radius500, + xxl: AppFlowyBorderRadiusConstant.radius600, + ); + } + + AppFlowySpacing buildSpacing( + AppFlowyBaseColorScheme colorScheme, + ) { + return AppFlowySpacing( + xs: AppFlowySpacingConstant.spacing100, + s: AppFlowySpacingConstant.spacing200, + m: AppFlowySpacingConstant.spacing300, + l: AppFlowySpacingConstant.spacing400, + xl: AppFlowySpacingConstant.spacing500, + xxl: AppFlowySpacingConstant.spacing600, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart new file mode 100644 index 0000000000..68fb378c0f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart @@ -0,0 +1,238 @@ +import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; +import 'package:appflowy_ui/src/theme/data/builder.dart'; +import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; +import 'package:appflowy_ui/src/theme/text_style/text_style.dart'; +import 'package:flutter/material.dart'; + +abstract class AppFlowyBaseTheme { + const AppFlowyBaseTheme(); + + AppFlowyBaseColorScheme get colorScheme; + + AppFlowyTextColorScheme get textColorScheme; + + AppFlowyBaseTextStyle get textStyle; + + AppFlowyIconColorTheme get iconColorTheme; + + AppFlowyBorderColorScheme get borderColorScheme; + + AppFlowyBackgroundColorScheme get backgroundColorScheme; + + AppFlowyFillColorScheme get fillColorScheme; + + AppFlowySurfaceColorScheme get surfaceColorScheme; + + AppFlowyBorderRadius get borderRadius; + + AppFlowySpacing get spacing; + + AppFlowyBrandColorScheme get brandColorScheme; +} + +class AppFlowyThemeData extends AppFlowyBaseTheme { + factory AppFlowyThemeData.light() { + final colorScheme = AppFlowyBaseColorScheme(); + + final textStyle = AppFlowyBaseTextStyle(); + final textColorScheme = themeBuilder.buildTextColorScheme( + colorScheme, + Brightness.light, + ); + final borderColorScheme = themeBuilder.buildBorderColorScheme( + colorScheme, + Brightness.light, + ); + final fillColorScheme = themeBuilder.buildFillColorScheme( + colorScheme, + Brightness.light, + ); + final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( + colorScheme, + Brightness.light, + ); + final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( + colorScheme, + Brightness.light, + ); + final iconColorTheme = themeBuilder.buildIconColorTheme( + colorScheme, + Brightness.light, + ); + final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); + final borderRadius = themeBuilder.buildBorderRadius(colorScheme); + final spacing = themeBuilder.buildSpacing(colorScheme); + + return AppFlowyThemeData( + colorScheme: colorScheme, + textColorScheme: textColorScheme, + textStyle: textStyle, + iconColorTheme: iconColorTheme, + backgroundColorScheme: backgroundColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + borderRadius: borderRadius, + spacing: spacing, + brandColorScheme: brandColorScheme, + ); + } + + factory AppFlowyThemeData.dark() { + final colorScheme = AppFlowyBaseColorScheme(); + final textStyle = AppFlowyBaseTextStyle(); + final textColorScheme = themeBuilder.buildTextColorScheme( + colorScheme, + Brightness.dark, + ); + final borderColorScheme = themeBuilder.buildBorderColorScheme( + colorScheme, + Brightness.dark, + ); + final fillColorScheme = themeBuilder.buildFillColorScheme( + colorScheme, + Brightness.dark, + ); + final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( + colorScheme, + Brightness.dark, + ); + final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( + colorScheme, + Brightness.dark, + ); + final iconColorTheme = themeBuilder.buildIconColorTheme( + colorScheme, + Brightness.dark, + ); + final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); + final borderRadius = themeBuilder.buildBorderRadius(colorScheme); + final spacing = themeBuilder.buildSpacing(colorScheme); + + return AppFlowyThemeData( + colorScheme: colorScheme, + textColorScheme: textColorScheme, + textStyle: textStyle, + iconColorTheme: iconColorTheme, + backgroundColorScheme: backgroundColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + borderRadius: borderRadius, + spacing: spacing, + brandColorScheme: brandColorScheme, + ); + } + + const AppFlowyThemeData({ + required this.colorScheme, + required this.textStyle, + required this.textColorScheme, + required this.borderColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.brandColorScheme, + required this.iconColorTheme, + required this.backgroundColorScheme, + this.brightness = Brightness.light, + }); + + static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); + + final Brightness brightness; + + @override + final AppFlowyBaseColorScheme colorScheme; + + @override + final AppFlowyBaseTextStyle textStyle; + + @override + final AppFlowyTextColorScheme textColorScheme; + + @override + final AppFlowyBorderColorScheme borderColorScheme; + + @override + final AppFlowyFillColorScheme fillColorScheme; + + @override + final AppFlowySurfaceColorScheme surfaceColorScheme; + + @override + final AppFlowyBorderRadius borderRadius; + + @override + final AppFlowySpacing spacing; + + @override + final AppFlowyBrandColorScheme brandColorScheme; + + @override + final AppFlowyIconColorTheme iconColorTheme; + + @override + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + static AppFlowyTextColorScheme buildTextColorScheme( + AppFlowyBaseColorScheme colorScheme, + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyTextColorScheme( + primary: colorScheme.neutral.neutral1000, + secondary: colorScheme.neutral.neutral600, + tertiary: colorScheme.neutral.neutral400, + quaternary: colorScheme.neutral.neutral200, + inverse: colorScheme.neutral.white, + onFill: colorScheme.neutral.white, + theme: colorScheme.blue.blue500, + themeHover: colorScheme.blue.blue600, + action: colorScheme.blue.blue500, + actionHover: colorScheme.blue.blue600, + info: colorScheme.blue.blue500, + infoHover: colorScheme.blue.blue600, + success: colorScheme.green.green600, + successHover: colorScheme.green.green700, + warning: colorScheme.orange.orange600, + warningHover: colorScheme.orange.orange700, + error: colorScheme.red.red600, + errorHover: colorScheme.red.red700, + purple: colorScheme.purple.purple500, + purpleHover: colorScheme.purple.purple600, + ), + Brightness.dark => AppFlowyTextColorScheme( + primary: colorScheme.neutral.neutral200, + secondary: colorScheme.neutral.neutral400, + tertiary: colorScheme.neutral.neutral600, + quaternary: colorScheme.neutral.neutral1000, + inverse: colorScheme.neutral.neutral1000, + onFill: colorScheme.neutral.white, + theme: colorScheme.blue.blue500, + themeHover: colorScheme.blue.blue600, + action: colorScheme.blue.blue500, + actionHover: colorScheme.blue.blue600, + info: colorScheme.blue.blue500, + infoHover: colorScheme.blue.blue600, + success: colorScheme.green.green600, + successHover: colorScheme.green.green700, + warning: colorScheme.orange.orange600, + warningHover: colorScheme.orange.orange700, + error: colorScheme.red.red500, + errorHover: colorScheme.red.red400, + purple: colorScheme.purple.purple500, + purpleHover: colorScheme.purple.purple600, + ), + }; + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart new file mode 100644 index 0000000000..e502b3b875 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart @@ -0,0 +1,17 @@ +class AppFlowySpacingConstant { + static const double spacing100 = 4; + static const double spacing200 = 6; + static const double spacing300 = 8; + static const double spacing400 = 12; + static const double spacing500 = 16; + static const double spacing600 = 20; +} + +class AppFlowyBorderRadiusConstant { + static const double radius100 = 4; + static const double radius200 = 6; + static const double radius300 = 8; + static const double radius400 = 12; + static const double radius500 = 16; + static const double radius600 = 20; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart new file mode 100644 index 0000000000..ea90784db3 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart @@ -0,0 +1,17 @@ +class AppFlowySpacing { + const AppFlowySpacing({ + required this.xs, + required this.s, + required this.m, + required this.l, + required this.xl, + required this.xxl, + }); + + final double xs; + final double s; + final double m; + final double l; + final double xl; + final double xxl; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart new file mode 100644 index 0000000000..a85ffbb6cb --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart @@ -0,0 +1,298 @@ +import 'package:flutter/widgets.dart'; + +abstract class TextThemeType { + const TextThemeType(); + + TextStyle standard({ + String family = '', + Color? color, + }); + TextStyle enhanced({ + String family = '', + Color? color, + }); + TextStyle prominent({ + String family = '', + Color? color, + }); + TextStyle underline({ + String family = '', + Color? color, + }); +} + +class TextThemeHeading { + const TextThemeHeading(); + + TextStyle h1({ + String family = '', + Color? color, + }) => + _defaultTextStyle( + family: family, + fontSize: 36, + height: 40 / 36, + color: color, + ); + + TextStyle h2({ + String family = '', + Color? color, + }) => + _defaultTextStyle( + family: family, + fontSize: 24, + height: 32 / 24, + color: color, + ); + + TextStyle h3({ + String family = '', + Color? color, + }) => + _defaultTextStyle( + family: family, + fontSize: 20, + height: 28 / 20, + color: color, + ); + + TextStyle h4({ + String family = '', + Color? color, + }) => + _defaultTextStyle( + family: family, + fontSize: 16, + height: 22 / 16, + color: color, + ); + + static TextStyle _defaultTextStyle({ + required String family, + required double fontSize, + required double height, + TextDecoration decoration = TextDecoration.none, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeadline extends TextThemeType { + const TextThemeHeadline(); + + @override + TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + ); + + @override + TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.bold, + ); + + @override + TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 24, + double height = 36 / 24, + FontWeight weight = FontWeight.normal, + TextDecoration decoration = TextDecoration.none, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + color: color, + ); +} + +class TextThemeTitle extends TextThemeType { + const TextThemeTitle(); + + @override + TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + ); + + @override + TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.bold, + ); + + @override + TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 20, + double height = 28 / 20, + FontWeight weight = FontWeight.normal, + TextDecoration decoration = TextDecoration.none, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + color: color, + ); +} + +class TextThemeBody extends TextThemeType { + const TextThemeBody(); + + @override + TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + ); + + @override + TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.bold, + ); + + @override + TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 14, + double height = 20 / 14, + FontWeight weight = FontWeight.normal, + TextDecoration decoration = TextDecoration.none, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + color: color, + ); +} + +class TextThemeCaption extends TextThemeType { + const TextThemeCaption(); + + @override + TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + ); + + @override + TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + weight: FontWeight.bold, + ); + + @override + TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + family: family, + color: color, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 12, + double height = 16 / 12, + FontWeight weight = FontWeight.normal, + TextDecoration decoration = TextDecoration.none, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + color: color, + ); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart new file mode 100644 index 0000000000..64daae2370 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart @@ -0,0 +1,17 @@ +import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart'; + +class AppFlowyBaseTextStyle { + const AppFlowyBaseTextStyle({ + this.heading = const TextThemeHeading(), + this.headline = const TextThemeHeadline(), + this.title = const TextThemeTitle(), + this.body = const TextThemeBody(), + this.caption = const TextThemeCaption(), + }); + + final TextThemeHeading heading; + final TextThemeType headline; + final TextThemeType title; + final TextThemeType body; + final TextThemeType caption; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart new file mode 100644 index 0000000000..b800b1bb6c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart @@ -0,0 +1,7 @@ +export 'appflowy_theme.dart'; +export 'border_radius/border_radius.dart'; +export 'color_scheme/color_scheme.dart'; +export 'data/data.dart'; +export 'dimensions.dart'; +export 'spacing/spacing.dart'; +export 'text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml new file mode 100644 index 0000000000..2f5633bb1e --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml @@ -0,0 +1,17 @@ +name: appflowy_ui +description: "A Flutter package for AppFlowy UI components and widgets" +version: 1.0.0 +homepage: https://github.com/appflowy-io/appflowy + +environment: + sdk: ^3.6.2 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_lints: ^5.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/frontend/resources/flowy_icons/20x/anonymous_mode.svg b/frontend/resources/flowy_icons/20x/anonymous_mode.svg new file mode 100644 index 0000000000..bee519e54a --- /dev/null +++ b/frontend/resources/flowy_icons/20x/anonymous_mode.svg @@ -0,0 +1,3 @@ + + + From c561abd9f8aaf2036c4b5fbd6a34e020dc08b254 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:52:44 +0800 Subject: [PATCH 271/384] fix: clear text field upon selection (#7695) --- .../cell/bloc/select_option_cell_editor_bloc.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart index f8ed915b62..c6e4e6484b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart @@ -241,6 +241,11 @@ class SelectOptionCellEditorBloc } else if (!state.selectedOptions .any((option) => option.id == focusedOptionId)) { _selectOptionService.select(optionIds: [focusedOptionId]); + emit( + state.copyWith( + clearFilter: true, + ), + ); } } From 995b773c7404abe9687c619033123d367e56cf93 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 7 Apr 2025 19:24:58 +0800 Subject: [PATCH 272/384] chore: replace str with uuid --- frontend/rust-lib/Cargo.lock | 88 +++---- frontend/rust-lib/Cargo.toml | 20 +- frontend/rust-lib/collab-integrate/Cargo.toml | 3 +- .../collab-integrate/src/collab_builder.rs | 24 +- .../src/persistence/collab_metadata_sql.rs | 15 +- .../src/document/document_event.rs | 6 +- .../src/folder_event.rs | 9 +- .../event-integration-test/src/lib.rs | 13 +- .../tests/chat/chat_message_test.rs | 10 +- .../tests/document/local_test/edit_test.rs | 6 +- frontend/rust-lib/flowy-ai-pub/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 57 +++-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 102 ++++---- frontend/rust-lib/flowy-ai/src/chat.rs | 17 +- frontend/rust-lib/flowy-ai/src/completion.rs | 112 ++++---- frontend/rust-lib/flowy-ai/src/entities.rs | 8 +- .../rust-lib/flowy-ai/src/event_handler.rs | 35 ++- .../flowy-ai/src/local_ai/controller.rs | 25 +- .../src/middleware/chat_service_mw.rs | 80 +++--- .../rust-lib/flowy-ai/src/notification.rs | 12 +- frontend/rust-lib/flowy-core/Cargo.toml | 4 +- .../flowy-core/src/deps_resolve/chat_deps.rs | 21 +- .../src/deps_resolve/cloud_service_impl.rs | 241 +++++++++--------- .../src/deps_resolve/collab_deps.rs | 5 +- .../src/deps_resolve/database_deps.rs | 27 +- .../src/deps_resolve/document_deps.rs | 4 +- .../src/deps_resolve/file_storage_deps.rs | 3 +- .../folder_deps/folder_deps_chat_impl.rs | 27 +- .../folder_deps/folder_deps_database_impl.rs | 69 +++-- .../folder_deps/folder_deps_doc_impl.rs | 31 ++- .../src/deps_resolve/folder_deps/mod.rs | 42 +-- .../flowy-core/src/deps_resolve/user_deps.rs | 3 +- frontend/rust-lib/flowy-core/src/lib.rs | 18 +- .../flowy-core/src/user_state_callback.rs | 10 +- .../rust-lib/flowy-database-pub/Cargo.toml | 4 +- .../rust-lib/flowy-database-pub/src/cloud.rs | 23 +- frontend/rust-lib/flowy-database2/Cargo.toml | 1 + .../flowy-database2/src/event_handler.rs | 4 +- .../rust-lib/flowy-database2/src/manager.rs | 109 +++++--- .../src/services/database/database_editor.rs | 8 +- .../src/services/database/database_observe.rs | 5 +- .../rust-lib/flowy-document-pub/Cargo.toml | 4 +- .../rust-lib/flowy-document-pub/src/cloud.rs | 16 +- .../rust-lib/flowy-document/src/document.rs | 7 +- .../rust-lib/flowy-document/src/entities.rs | 43 ++-- .../flowy-document/src/event_handler.rs | 12 +- .../rust-lib/flowy-document/src/manager.rs | 69 ++--- .../src/parser/parser_entities.rs | 6 +- .../tests/document/document_redo_undo_test.rs | 4 +- .../tests/document/document_test.rs | 16 +- .../flowy-document/tests/document/util.rs | 51 ++-- frontend/rust-lib/flowy-error/Cargo.toml | 1 + frontend/rust-lib/flowy-error/src/errors.rs | 6 + .../rust-lib/flowy-folder-pub/src/cloud.rs | 40 +-- .../rust-lib/flowy-folder-pub/src/query.rs | 9 +- .../flowy-folder/src/entities/import.rs | 4 + .../flowy-folder/src/entities/view.rs | 50 ++-- .../flowy-folder/src/event_handler.rs | 16 +- frontend/rust-lib/flowy-folder/src/manager.rs | 166 ++++++------ .../rust-lib/flowy-folder/src/manager_init.rs | 26 +- .../flowy-folder/src/manager_observer.rs | 57 +++-- .../rust-lib/flowy-folder/src/notification.rs | 12 +- .../rust-lib/flowy-folder/src/share/import.rs | 3 +- frontend/rust-lib/flowy-folder/src/util.rs | 3 +- .../flowy-folder/src/view_operation.rs | 24 +- frontend/rust-lib/flowy-search-pub/Cargo.toml | 2 +- .../rust-lib/flowy-search-pub/src/cloud.rs | 3 +- .../rust-lib/flowy-search-pub/src/entities.rs | 15 +- frontend/rust-lib/flowy-search/Cargo.toml | 8 +- .../flowy-search/src/document/handler.rs | 16 +- .../flowy-search/src/folder/indexer.rs | 20 +- .../flowy-server/src/af_cloud/define.rs | 3 +- .../flowy-server/src/af_cloud/impls/chat.rs | 94 +++---- .../src/af_cloud/impls/database.rs | 70 +++-- .../src/af_cloud/impls/document.rs | 38 +-- .../src/af_cloud/impls/file_storage.rs | 15 +- .../flowy-server/src/af_cloud/impls/folder.rs | 81 +++--- .../flowy-server/src/af_cloud/impls/search.rs | 3 +- .../af_cloud/impls/user/cloud_service_impl.rs | 87 +++---- .../flowy-server/src/af_cloud/impls/util.rs | 7 +- .../rust-lib/flowy-server/src/default_impl.rs | 97 +++---- .../src/local_server/impls/database.rs | 32 +-- .../src/local_server/impls/document.rs | 21 +- .../src/local_server/impls/folder.rs | 58 ++--- .../src/local_server/impls/user.rs | 27 +- .../flowy-server/tests/af_cloud_test/util.rs | 2 +- .../rust-lib/flowy-storage-pub/Cargo.toml | 3 +- .../rust-lib/flowy-storage-pub/src/cloud.rs | 23 +- frontend/rust-lib/flowy-storage/Cargo.toml | 4 +- .../rust-lib/flowy-storage/src/manager.rs | 41 ++- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 28 +- .../rust-lib/flowy-user-pub/src/entities.rs | 6 + .../flowy-user-pub/src/workspace_service.rs | 3 +- .../rust-lib/flowy-user/src/event_handler.rs | 25 +- .../src/services/authenticate_user.rs | 12 +- .../flowy-user/src/services/billing_check.rs | 5 +- .../data_import/appflowy_data_import.rs | 6 +- .../flowy-user/src/user_manager/manager.rs | 6 +- .../user_manager/manager_user_awareness.rs | 52 ++-- .../user_manager/manager_user_workspace.rs | 46 ++-- 100 files changed, 1560 insertions(+), 1349 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 69d6220409..e10038c7d3 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -496,7 +496,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "bincode", @@ -516,16 +516,16 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "bytes", "futures", - "pin-project", "serde", "serde_json", "serde_repr", "thiserror 1.0.64", + "uuid", ] [[package]] @@ -1137,7 +1137,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "again", "anyhow", @@ -1192,7 +1192,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1205,7 +1205,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "futures-channel", "futures-util", @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "arc-swap", @@ -1273,7 +1273,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "async-trait", @@ -1313,7 +1313,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "arc-swap", @@ -1334,7 +1334,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "bytes", @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "arc-swap", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "async-recursion", @@ -1418,7 +1418,6 @@ version = "0.1.0" dependencies = [ "anyhow", "arc-swap", - "async-trait", "collab", "collab-database", "collab-document", @@ -1429,18 +1428,18 @@ dependencies = [ "diesel", "flowy-error", "flowy-sqlite", - "futures", "lib-infra", "serde", "serde_json", "tokio", "tracing", + "uuid", ] [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "async-stream", @@ -1478,7 +1477,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "bincode", @@ -1500,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "async-trait", @@ -1511,13 +1510,14 @@ dependencies = [ "thiserror 1.0.64", "tokio", "tracing", + "uuid", "yrs", ] [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" dependencies = [ "anyhow", "collab", @@ -1764,7 +1764,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1947,7 +1947,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "bincode", "bytes", @@ -2513,13 +2513,13 @@ dependencies = [ name = "flowy-ai-pub" version = "0.1.0" dependencies = [ - "bytes", "client-api", "flowy-error", "futures", "lib-infra", "serde", "serde_json", + "uuid", ] [[package]] @@ -2592,7 +2592,6 @@ dependencies = [ "flowy-storage-pub", "flowy-user", "flowy-user-pub", - "futures", "futures-core", "lib-dispatch", "lib-infra", @@ -2606,19 +2605,18 @@ dependencies = [ "tokio-stream", "tracing", "uuid", - "walkdir", ] [[package]] name = "flowy-database-pub" version = "0.1.0" dependencies = [ - "anyhow", "client-api", "collab", "collab-entity", "flowy-error", "lib-infra", + "uuid", ] [[package]] @@ -2667,6 +2665,7 @@ dependencies = [ "tokio-util", "tracing", "url", + "uuid", "validator 0.18.1", ] @@ -2744,11 +2743,11 @@ dependencies = [ name = "flowy-document-pub" version = "0.1.0" dependencies = [ - "anyhow", "collab", "collab-document", "flowy-error", "lib-infra", + "uuid", ] [[package]] @@ -2778,6 +2777,7 @@ dependencies = [ "thiserror 1.0.64", "tokio", "url", + "uuid", "validator 0.18.1", ] @@ -2864,16 +2864,12 @@ dependencies = [ "bytes", "collab", "collab-folder", - "diesel", - "diesel_derives", - "diesel_migrations", "flowy-codegen", "flowy-derive", "flowy-error", "flowy-folder", "flowy-notification", "flowy-search-pub", - "flowy-sqlite", "flowy-user", "futures", "lib-dispatch", @@ -2887,7 +2883,7 @@ dependencies = [ "tempfile", "tokio", "tracing", - "validator 0.18.1", + "uuid", ] [[package]] @@ -2898,8 +2894,8 @@ dependencies = [ "collab", "collab-folder", "flowy-error", - "futures", "lib-infra", + "uuid", ] [[package]] @@ -2991,7 +2987,6 @@ name = "flowy-storage" version = "0.1.0" dependencies = [ "allo-isolate", - "anyhow", "async-trait", "bytes", "chrono", @@ -3003,8 +2998,6 @@ dependencies = [ "flowy-notification", "flowy-sqlite", "flowy-storage-pub", - "futures-util", - "fxhash", "lib-dispatch", "lib-infra", "mime_guess", @@ -3032,9 +3025,8 @@ dependencies = [ "mime", "mime_guess", "serde", - "serde_json", "tokio", - "tracing", + "uuid", ] [[package]] @@ -3432,7 +3424,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3447,7 +3439,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "app-error", "jsonwebtoken", @@ -4068,7 +4060,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "bytes", @@ -5181,7 +5173,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5201,7 +5193,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5269,19 +5260,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6782,7 +6760,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 9245232f29..e3cb5a4178 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" } [profile.dev] opt-level = 0 @@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index b817d639f1..0cbdd41ccd 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -19,14 +19,13 @@ serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true -async-trait.workspace = true tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } -futures = "0.3.31" arc-swap = "1.7" flowy-sqlite = { workspace = true } diesel.workspace = true flowy-error.workspace = true +uuid.workspace = true [features] default = [] diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index c94660dbfd..226a5b679b 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -1,5 +1,6 @@ use std::borrow::BorrowMut; use std::fmt::{Debug, Display}; +use std::str::FromStr; use std::sync::{Arc, Weak}; use crate::CollabKVDB; @@ -33,8 +34,10 @@ use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::local_storage::CollabPersistenceConfig; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; +use flowy_error::FlowyError; use lib_infra::{if_native, if_wasm}; use tracing::{error, instrument, trace, warn}; +use uuid::Uuid; #[derive(Clone, Debug)] pub enum CollabPluginProviderType { @@ -66,8 +69,8 @@ impl Display for CollabPluginProviderContext { } pub trait WorkspaceCollabIntegrate: Send + Sync { - fn workspace_id(&self) -> Result; - fn device_id(&self) -> Result; + fn workspace_id(&self) -> Result; + fn device_id(&self) -> Result; } pub struct AppFlowyCollabBuilder { @@ -119,15 +122,15 @@ impl AppFlowyCollabBuilder { pub fn collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, ) -> Result { // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match. // This check is crucial in asynchronous code contexts where the workspace_id might change during operation. let actual_workspace_id = self.workspace_integrate.workspace_id()?; - if workspace_id != actual_workspace_id { + if workspace_id != &actual_workspace_id { return Err(anyhow::anyhow!( "workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}", workspace_id, @@ -140,7 +143,7 @@ impl AppFlowyCollabBuilder { uid, object_id.to_string(), collab_type, - workspace_id, + workspace_id.to_string(), device_id, )) } @@ -399,11 +402,11 @@ impl CollabBuilderConfig { pub struct CollabPersistenceImpl { pub db: Weak, pub uid: i64, - pub workspace_id: String, + pub workspace_id: Uuid, } impl CollabPersistenceImpl { - pub fn new(db: Weak, uid: i64, workspace_id: String) -> Self { + pub fn new(db: Weak, uid: i64, workspace_id: Uuid) -> Self { Self { db, uid, @@ -423,7 +426,8 @@ impl CollabPersistence for CollabPersistenceImpl { .upgrade() .ok_or_else(|| CollabError::Internal(anyhow!("collab_db is dropped")))?; - let object_id = collab.object_id().to_string(); + let object_id = + Uuid::from_str(collab.object_id()).map_err(|v| CollabError::Internal(v.into()))?; let rocksdb_read = collab_db.read_txn(); if rocksdb_read.is_exist(self.uid, &self.workspace_id, &object_id) { @@ -461,7 +465,7 @@ impl CollabPersistence for CollabPersistenceImpl { write_txn .flush_doc( self.uid, - self.workspace_id.as_str(), + self.workspace_id.to_string().as_str(), object_id, encoded_collab.state_vector.to_vec(), encoded_collab.doc_state.to_vec(), diff --git a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs index 82e993fc49..bc5f28e642 100644 --- a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs +++ b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs @@ -7,6 +7,8 @@ use flowy_sqlite::{ DBConnection, ExpressionMethods, Identifiable, Insertable, Queryable, }; use std::collections::HashMap; +use std::str::FromStr; +use uuid::Uuid; #[derive(Queryable, Insertable, Identifiable)] #[diesel(table_name = af_collab_metadata)] @@ -43,13 +45,18 @@ pub fn batch_insert_collab_metadata( pub fn batch_select_collab_metadata( mut conn: DBConnection, - object_ids: &[String], -) -> FlowyResult> { + object_ids: &[Uuid], +) -> FlowyResult> { + let object_ids = object_ids + .iter() + .map(|id| id.to_string()) + .collect::>(); + let metadata = dsl::af_collab_metadata - .filter(af_collab_metadata::object_id.eq_any(object_ids)) + .filter(af_collab_metadata::object_id.eq_any(&object_ids)) .load::(&mut conn)? .into_iter() - .map(|m| (m.object_id.clone(), m)) + .flat_map(|m| Uuid::from_str(&m.object_id).and_then(|v| Ok((v, m)))) .collect(); Ok(metadata) } diff --git a/frontend/rust-lib/event-integration-test/src/document/document_event.rs b/frontend/rust-lib/event-integration-test/src/document/document_event.rs index 65b4943f80..28fb03e9ed 100644 --- a/frontend/rust-lib/event-integration-test/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document/document_event.rs @@ -1,8 +1,6 @@ use collab::entity::EncodedCollab; use std::collections::HashMap; -use serde_json::Value; - use flowy_document::entities::*; use flowy_document::event_map::DocumentEvent; use flowy_document::parser::parser_entities::{ @@ -11,6 +9,8 @@ use flowy_document::parser::parser_entities::{ }; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder::event_map::FolderEvent; +use serde_json::Value; +use uuid::Uuid; use crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data}; use crate::event_builder::EventBuilder; @@ -37,7 +37,7 @@ impl DocumentEventTest { Self { event_test: core } } - pub async fn get_encoded_v1(&self, doc_id: &str) -> EncodedCollab { + pub async fn get_encoded_v1(&self, doc_id: &Uuid) -> EncodedCollab { let doc = self .event_test .appflowy_core diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index fae34e175c..2e1b1cc417 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,4 +1,5 @@ use flowy_folder::view_operation::{GatherEncodedCollab, ViewData}; +use std::str::FromStr; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -16,6 +17,7 @@ use flowy_user::entities::{ use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; use flowy_user_pub::entities::Role; +use uuid::Uuid; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; @@ -123,10 +125,10 @@ impl EventIntegrationTest { let create_view_params = views .into_iter() .map(|view| CreateViewParams { - parent_view_id: view.parent_view_id, + parent_view_id: Uuid::from_str(&view.parent_view_id).unwrap(), name: view.name, layout: view.layout.into(), - view_id: view.id, + view_id: Uuid::from_str(&view.id).unwrap(), initial_data: ViewData::Empty, meta: Default::default(), set_as_current: false, @@ -195,9 +197,10 @@ impl EventIntegrationTest { view_id: &str, layout: ViewLayout, ) -> GatherEncodedCollab { + let view_id = Uuid::from_str(view_id).unwrap(); self .folder_manager - .gather_publish_encode_collab(view_id, &layout) + .gather_publish_encode_collab(&view_id, &layout) .await .unwrap() } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 573c8b692b..02efc0f75a 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -1,3 +1,4 @@ +use crate::user_event::TestNotificationSender; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; @@ -15,14 +16,14 @@ use nanoid::nanoid; use semver::Version; use std::env::temp_dir; use std::path::PathBuf; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::select; use tokio::task::LocalSet; use tokio::time::sleep; - -use crate::user_event::TestNotificationSender; +use uuid::Uuid; mod chat_event; pub mod database_event; @@ -145,10 +146,16 @@ impl EventIntegrationTest { ) -> Result, FlowyError> { let server = self.server_provider.get_server().unwrap(); let workspace_id = self.get_current_workspace().await.id; + let oid = Uuid::from_str(oid).unwrap(); let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_folder_doc_state(&workspace_id, uid, collab_type, oid) + .get_folder_doc_state( + &Uuid::from_str(&workspace_id).unwrap(), + uid, + collab_type, + &oid, + ) .await?; Ok(doc_state) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index 1a5f9356c4..c5b30d68c3 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -3,10 +3,12 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_ai::entities::ChatMessageListPB; use flowy_ai::notification::ChatNotification; +use std::str::FromStr; use flowy_ai_pub::cloud::ChatMessageType; use std::time::Duration; +use uuid::Uuid; #[tokio::test] async fn af_cloud_create_chat_message_test() { @@ -21,8 +23,8 @@ async fn af_cloud_create_chat_message_test() { for i in 0..10 { let _ = chat_service .create_question( - ¤t_workspace.id, - &chat_id, + &Uuid::from_str(¤t_workspace.id).unwrap(), + &Uuid::from_str(&chat_id).unwrap(), &format!("hello world {}", i), ChatMessageType::System, &[], @@ -77,8 +79,8 @@ async fn af_cloud_load_remote_system_message_test() { for i in 0..10 { let _ = chat_service .create_question( - ¤t_workspace.id, - &chat_id, + &Uuid::from_str(¤t_workspace.id).unwrap(), + &Uuid::from_str(&chat_id).unwrap(), &format!("hello server {}", i), ChatMessageType::System, &[], diff --git a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs index 199c1b43c2..d9273dbe8b 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs @@ -8,6 +8,8 @@ use flowy_document::parser::parser_entities::{ }; use serde_json::{json, Value}; use std::collections::HashMap; +use std::str::FromStr; +use uuid::Uuid; #[tokio::test] async fn get_document_event_test() { @@ -101,8 +103,8 @@ async fn document_size_test() { let s = generate_random_string(string_size); test.insert_index(&view.id, &s, 1, None).await; } - - let encoded_v1 = test.get_encoded_v1(&view.id).await; + let view_id = Uuid::from_str(&view.id).unwrap(); + let encoded_v1 = test.get_encoded_v1(&view_id).await; if encoded_v1.doc_state.len() > max_size { panic!( "The document size is too large. {}", diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml index 09ca8dc2d4..dfb67490ac 100644 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-ai-pub/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" lib-infra = { workspace = true } flowy-error = { workspace = true } client-api = { workspace = true } -bytes.workspace = true futures.workspace = true serde_json.workspace = true -serde.workspace = true \ No newline at end of file +serde.workspace = true +uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 7eb8386de1..79478f64fc 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; +use uuid::Uuid; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; @@ -81,15 +82,15 @@ pub trait ChatCloudService: Send + Sync + 'static { async fn create_chat( &self, uid: &i64, - workspace_id: &str, - chat_id: &str, - rag_ids: Vec, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, ) -> Result<(), FlowyError>; async fn create_question( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, metadata: &[ChatMessageMetadata], @@ -97,8 +98,8 @@ pub trait ChatCloudService: Send + Sync + 'static { async fn create_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -106,8 +107,8 @@ pub trait ChatCloudService: Send + Sync + 'static { async fn stream_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, format: ResponseFormat, ai_model: Option, @@ -115,68 +116,68 @@ pub trait ChatCloudService: Send + Sync + 'static { async fn get_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, question_message_id: i64, ) -> Result; async fn get_chat_messages( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, offset: MessageCursor, limit: u64, ) -> Result; async fn get_question_from_answer_id( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, answer_message_id: i64, ) -> Result; async fn get_related_message( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, ) -> Result; async fn stream_complete( &self, - workspace_id: &str, + workspace_id: &Uuid, params: CompleteTextParams, ai_model: Option, ) -> Result; async fn embed_file( &self, - workspace_id: &str, + workspace_id: &Uuid, file_path: &Path, - chat_id: &str, + chat_id: &Uuid, metadata: Option>, ) -> Result<(), FlowyError>; - async fn get_local_ai_config(&self, workspace_id: &str) -> Result; + async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result; async fn get_workspace_plan( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError>; async fn get_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, ) -> Result; async fn update_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, params: UpdateChatParams, ) -> Result<(), FlowyError>; - async fn get_available_models(&self, workspace_id: &str) -> Result; - async fn get_workspace_default_model(&self, workspace_id: &str) -> Result; + async fn get_available_models(&self, workspace_id: &Uuid) -> Result; + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result; } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 5e50f768f1..e11f2328e4 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -27,14 +27,16 @@ use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; use std::path::PathBuf; +use std::str::FromStr; use std::sync::{Arc, Weak}; use tokio::sync::RwLock; use tracing::{error, info, instrument, trace}; +use uuid::Uuid; pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; fn device_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; } @@ -44,18 +46,18 @@ pub trait AIUserService: Send + Sync + 'static { pub trait AIExternalService: Send + Sync + 'static { async fn query_chat_rag_ids( &self, - parent_view_id: &str, - chat_id: &str, - ) -> Result, FlowyError>; + parent_view_id: &Uuid, + chat_id: &Uuid, + ) -> Result, FlowyError>; async fn sync_rag_documents( &self, - workspace_id: &str, - rag_ids: Vec, - rag_metadata_map: HashMap, + workspace_id: &Uuid, + rag_ids: Vec, + rag_metadata_map: HashMap, ) -> Result, FlowyError>; - async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError>; + async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError>; } #[derive(Debug, Default)] @@ -70,7 +72,7 @@ pub struct AIManager { pub cloud_service_wm: Arc, pub user_service: Arc, pub external_service: Arc, - chats: Arc>>, + chats: Arc>>, pub local_ai: Arc, pub store_preferences: Arc, server_models: Arc>, @@ -132,11 +134,11 @@ impl AIManager { Ok(()) } - pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> { - self.chats.entry(chat_id.to_string()).or_insert_with(|| { + pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { + self.chats.entry(chat_id.clone()).or_insert_with(|| { Arc::new(Chat::new( self.user_service.user_id().unwrap(), - chat_id.to_string(), + chat_id.clone(), self.user_service.clone(), self.cloud_service_wm.clone(), )) @@ -150,7 +152,7 @@ impl AIManager { let cloud_service_wm = self.cloud_service_wm.clone(); let store_preferences = self.store_preferences.clone(); let external_service = self.external_service.clone(); - let chat_id = chat_id.to_string(); + let chat_id = chat_id.clone(); tokio::spawn(async move { match refresh_chat_setting( &user_service, @@ -161,7 +163,12 @@ impl AIManager { .await { Ok(settings) => { - let _ = sync_chat_documents(user_service, external_service, settings.rag_ids).await; + let rag_ids = settings + .rag_ids + .into_iter() + .flat_map(|r| Uuid::from_str(&r).ok()) + .collect(); + let _ = sync_chat_documents(user_service, external_service, rag_ids).await; }, Err(err) => { error!("failed to refresh chat settings: {}", err); @@ -172,13 +179,13 @@ impl AIManager { Ok(()) } - pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> { + pub async fn close_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { trace!("close chat: {}", chat_id); self.local_ai.close_chat(chat_id); Ok(()) } - pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> { + pub async fn delete_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { if let Some((_, chat)) = self.chats.remove(chat_id) { chat.close(); @@ -212,8 +219,8 @@ impl AIManager { pub async fn create_chat( &self, uid: &i64, - parent_view_id: &str, - chat_id: &str, + parent_view_id: &Uuid, + chat_id: &Uuid, ) -> Result, FlowyError> { let workspace_id = self.user_service.workspace_id()?; let rag_ids = self @@ -231,11 +238,11 @@ impl AIManager { let chat = Arc::new(Chat::new( self.user_service.user_id()?, - chat_id.to_string(), + chat_id.clone(), self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(chat_id.to_string(), chat.clone()); + self.chats.insert(chat_id.clone(), chat.clone()); Ok(chat) } @@ -244,7 +251,7 @@ impl AIManager { params: StreamMessageParams, ) -> Result { let chat = self.get_or_create_chat_instance(¶ms.chat_id).await?; - let ai_model = self.get_active_model(¶ms.chat_id).await; + let ai_model = self.get_active_model(¶ms.chat_id.to_string()).await; let question = chat.stream_chat_message(¶ms, ai_model).await?; let _ = self .external_service @@ -255,7 +262,7 @@ impl AIManager { pub async fn stream_regenerate_response( &self, - chat_id: &str, + chat_id: &Uuid, answer_message_id: i64, answer_stream_port: i64, format: Option, @@ -270,7 +277,7 @@ impl AIManager { || { self .store_preferences - .get_object::(&ai_available_models_key(chat_id)) + .get_object::(&ai_available_models_key(&chat_id.to_string())) }, |model| Some(model.into()), ); @@ -520,17 +527,17 @@ impl AIManager { }) } - pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result, FlowyError> { + pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result, FlowyError> { let chat = self.chats.get(chat_id).as_deref().cloned(); match chat { None => { let chat = Arc::new(Chat::new( self.user_service.user_id()?, - chat_id.to_string(), + chat_id.clone(), self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(chat_id.to_string(), chat.clone()); + self.chats.insert(chat_id.clone(), chat.clone()); Ok(chat) }, Some(chat) => Ok(chat), @@ -554,7 +561,7 @@ impl AIManager { pub async fn load_prev_chat_messages( &self, - chat_id: &str, + chat_id: &Uuid, limit: i64, before_message_id: Option, ) -> Result { @@ -567,7 +574,7 @@ impl AIManager { pub async fn load_latest_chat_messages( &self, - chat_id: &str, + chat_id: &Uuid, limit: i64, after_message_id: Option, ) -> Result { @@ -580,7 +587,7 @@ impl AIManager { pub async fn get_related_questions( &self, - chat_id: &str, + chat_id: &Uuid, message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -590,7 +597,7 @@ impl AIManager { pub async fn generate_answer( &self, - chat_id: &str, + chat_id: &Uuid, question_message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -598,19 +605,19 @@ impl AIManager { Ok(resp) } - pub async fn stop_stream(&self, chat_id: &str) -> Result<(), FlowyError> { + pub async fn stop_stream(&self, chat_id: &Uuid) -> Result<(), FlowyError> { let chat = self.get_or_create_chat_instance(chat_id).await?; chat.stop_stream_message().await; Ok(()) } - pub async fn chat_with_file(&self, chat_id: &str, file_path: PathBuf) -> FlowyResult<()> { + pub async fn chat_with_file(&self, chat_id: &Uuid, file_path: PathBuf) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; chat.index_file(file_path).await?; Ok(()) } - pub async fn get_rag_ids(&self, chat_id: &str) -> FlowyResult> { + pub async fn get_rag_ids(&self, chat_id: &Uuid) -> FlowyResult> { if let Some(settings) = self .store_preferences .get_object::(&setting_store_key(chat_id)) @@ -628,7 +635,7 @@ impl AIManager { Ok(settings.rag_ids) } - pub async fn update_rag_ids(&self, chat_id: &str, rag_ids: Vec) -> FlowyResult<()> { + pub async fn update_rag_ids(&self, chat_id: &Uuid, rag_ids: Vec) -> FlowyResult<()> { info!("[Chat] update chat:{} rag ids: {:?}", chat_id, rag_ids); let workspace_id = self.user_service.workspace_id()?; let update_setting = UpdateChatParams { @@ -659,6 +666,10 @@ impl AIManager { let user_service = self.user_service.clone(); let external_service = self.external_service.clone(); + let rag_ids = rag_ids + .into_iter() + .flat_map(|r| Uuid::from_str(&r).ok()) + .collect(); sync_chat_documents(user_service, external_service, rag_ids).await?; Ok(()) } @@ -667,7 +678,7 @@ impl AIManager { async fn sync_chat_documents( user_service: Arc, external_service: Arc, - rag_ids: Vec, + rag_ids: Vec, ) -> FlowyResult<()> { if rag_ids.is_empty() { return Ok(()); @@ -697,7 +708,7 @@ async fn sync_chat_documents( Ok(()) } -fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { +fn save_chat(conn: DBConnection, chat_id: &Uuid) -> FlowyResult<()> { let row = ChatTable { chat_id: chat_id.to_string(), created_at: timestamp(), @@ -716,7 +727,7 @@ async fn refresh_chat_setting( user_service: &Arc, cloud_service: &Arc, store_preferences: &Arc, - chat_id: &str, + chat_id: &Uuid, ) -> FlowyResult { info!("[Chat] refresh chat:{} setting", chat_id); let workspace_id = user_service.workspace_id()?; @@ -728,15 +739,18 @@ async fn refresh_chat_setting( error!("failed to set chat settings: {}", err); } - chat_notification_builder(chat_id, ChatNotification::DidUpdateChatSettings) - .payload(ChatSettingsPB { - rag_ids: settings.rag_ids.clone(), - }) - .send(); + chat_notification_builder( + &chat_id.to_string(), + ChatNotification::DidUpdateChatSettings, + ) + .payload(ChatSettingsPB { + rag_ids: settings.rag_ids.clone(), + }) + .send(); Ok(settings) } -fn setting_store_key(chat_id: &str) -> String { - format!("chat_settings_{}", chat_id) +fn setting_store_key(chat_id: &Uuid) -> String { + format!("chat_settings_{}", chat_id.to_string()) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index e00f21a863..d893ff2b51 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -23,6 +23,7 @@ use std::sync::atomic::{AtomicBool, AtomicI64}; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use tracing::{error, instrument, trace}; +use uuid::Uuid; enum PrevMessageState { HasMore, @@ -31,7 +32,7 @@ enum PrevMessageState { } pub struct Chat { - chat_id: String, + chat_id: Uuid, uid: i64, user_service: Arc, chat_service: Arc, @@ -44,7 +45,7 @@ pub struct Chat { impl Chat { pub fn new( uid: i64, - chat_id: String, + chat_id: Uuid, user_service: Arc, chat_service: Arc, ) -> Chat { @@ -197,7 +198,7 @@ impl Chat { answer_stream_port: i64, answer_stream_buffer: Arc>, uid: i64, - workspace_id: String, + workspace_id: Uuid, question_id: i64, format: ResponseFormat, ai_model: Option, @@ -254,7 +255,7 @@ impl Chat { .send(StreamMessage::OnError(err.msg.clone()).to_string()) .await; let pb = ChatMessageErrorPB { - chat_id: chat_id.clone(), + chat_id: chat_id.to_string(), error_message: err.to_string(), }; chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) @@ -293,7 +294,7 @@ impl Chat { } let pb = ChatMessageErrorPB { - chat_id: chat_id.clone(), + chat_id: chat_id.to_string(), error_message: err.to_string(), }; chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) @@ -566,7 +567,7 @@ impl Chat { let conn = self.user_service.sqlite_connection(self.uid)?; let records = select_chat_messages( conn, - &self.chat_id, + &self.chat_id.to_string(), limit, after_message_id, before_message_id, @@ -628,7 +629,7 @@ impl Chat { fn save_chat_message_disk( conn: DBConnection, - chat_id: &str, + chat_id: &Uuid, messages: Vec, ) -> FlowyResult<()> { let records = messages @@ -683,7 +684,7 @@ impl StringBuffer { pub(crate) fn save_and_notify_message( uid: i64, - chat_id: &str, + chat_id: &Uuid, user_service: &Arc, message: ChatMessage, ) -> Result<(), FlowyError> { diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 27babfb28e..53d2f3915e 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -1,6 +1,7 @@ use crate::ai_manager::AIUserService; use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB}; use allo_isolate::Isolate; +use std::str::FromStr; use dashmap::DashMap; use flowy_ai_pub::cloud::{ @@ -15,7 +16,8 @@ use lib_infra::isolate_stream::IsolateSink; use crate::stream_message::StreamMessage; use std::sync::{Arc, Weak}; use tokio::select; -use tracing::info; +use tracing::{error, info}; +use uuid::Uuid; pub struct AICompletion { tasks: Arc>>, @@ -77,7 +79,7 @@ impl AICompletion { } pub struct CompletionTask { - workspace_id: String, + workspace_id: Uuid, task_id: String, stop_rx: tokio::sync::mpsc::Receiver<()>, context: CompleteTextPB, @@ -87,7 +89,7 @@ pub struct CompletionTask { impl CompletionTask { pub fn new( - workspace_id: String, + workspace_id: Uuid, context: CompleteTextPB, preferred_model: Option, cloud_service: Weak, @@ -122,59 +124,63 @@ impl CompletionTask { let _ = sink.send("start:".to_string()).await; let completion_history = Some(self.context.history.iter().map(Into::into).collect()); let format = self.context.format.map(Into::into).unwrap_or_default(); - let params = CompleteTextParams { - text: self.context.text, - completion_type: Some(complete_type), - metadata: Some(CompletionMetadata { - object_id: self.context.object_id, - workspace_id: Some(self.workspace_id.clone()), - rag_ids: Some(self.context.rag_ids), - completion_history, - custom_prompt: self - .context - .custom_prompt - .map(|v| CustomPrompt { system: v }), - }), - format, - }; + if let Ok(object_id) = Uuid::from_str(&self.context.object_id) { + let params = CompleteTextParams { + text: self.context.text, + completion_type: Some(complete_type), + metadata: Some(CompletionMetadata { + object_id, + workspace_id: Some(self.workspace_id.clone()), + rag_ids: Some(self.context.rag_ids), + completion_history, + custom_prompt: self + .context + .custom_prompt + .map(|v| CustomPrompt { system: v }), + }), + format, + }; - info!("start completion: {:?}", params); - match cloud_service - .stream_complete(&self.workspace_id, params, self.preferred_model) - .await - { - Ok(mut stream) => loop { - select! { - _ = self.stop_rx.recv() => { - return; - }, - result = stream.next() => { - match result { - Some(Ok(data)) => { - match data { - CompletionStreamValue::Answer{ value } => { - let _ = sink.send(format!("data:{}", value)).await; + info!("start completion: {:?}", params); + match cloud_service + .stream_complete(&self.workspace_id, params, self.preferred_model) + .await + { + Ok(mut stream) => loop { + select! { + _ = self.stop_rx.recv() => { + return; + }, + result = stream.next() => { + match result { + Some(Ok(data)) => { + match data { + CompletionStreamValue::Answer{ value } => { + let _ = sink.send(format!("data:{}", value)).await; + } + CompletionStreamValue::Comment{ value } => { + let _ = sink.send(format!("comment:{}", value)).await; + } } - CompletionStreamValue::Comment{ value } => { - let _ = sink.send(format!("comment:{}", value)).await; - } - } - }, - Some(Err(error)) => { - handle_error(&mut sink, error).await; - return; - }, - None => { - let _ = sink.send(format!("finish:{}", self.task_id)).await; - return; - }, + }, + Some(Err(error)) => { + handle_error(&mut sink, error).await; + return; + }, + None => { + let _ = sink.send(format!("finish:{}", self.task_id)).await; + return; + }, + } } - } - } - }, - Err(error) => { - handle_error(&mut sink, error).await; - }, + } + }, + Err(error) => { + handle_error(&mut sink, error).await; + }, + } + } else { + error!("Invalid uuid: {}", self.context.object_id); } } }); diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 0075e35c04..c48fbd9646 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -1,8 +1,6 @@ -use af_plugin::core::plugin::RunningState; -use std::collections::HashMap; - use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; +use af_plugin::core::plugin::RunningState; use flowy_ai_pub::cloud::{ AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, @@ -10,6 +8,8 @@ use flowy_ai_pub::cloud::{ }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; +use std::collections::HashMap; +use uuid::Uuid; use validator::Validate; #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -78,7 +78,7 @@ pub struct StreamChatPayloadPB { #[derive(Default, Debug)] pub struct StreamMessageParams { - pub chat_id: String, + pub chat_id: Uuid, pub message: String, pub message_type: ChatMessageType, pub answer_stream_port: i64, diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 72a271f5a1..b8334ffe8d 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -1,6 +1,3 @@ -use std::fs; -use std::path::PathBuf; - use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; use crate::completion::AICompletion; use crate::entities::*; @@ -10,8 +7,12 @@ use flowy_ai_pub::cloud::{ }; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; use std::sync::{Arc, Weak}; use tracing::trace; +use uuid::Uuid; use validator::Validate; fn upgrade_ai_manager(ai_manager: AFPluginState>) -> FlowyResult> { @@ -70,6 +71,7 @@ pub(crate) async fn stream_chat_message_handler( trace!("Stream chat message with metadata: {:?}", metadata); + let chat_id = Uuid::from_str(&chat_id)?; let params = StreamMessageParams { chat_id, message, @@ -91,11 +93,12 @@ pub(crate) async fn regenerate_response_handler( ai_manager: AFPluginState>, ) -> FlowyResult<()> { let data = data.try_into_inner()?; + let chat_id = Uuid::from_str(&data.chat_id)?; let ai_manager = upgrade_ai_manager(ai_manager)?; ai_manager .stream_regenerate_response( - &data.chat_id, + &chat_id, data.answer_message_id, data.answer_stream_port, data.format, @@ -147,8 +150,9 @@ pub(crate) async fn load_prev_message_handler( let data = data.into_inner(); data.validate()?; + let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_prev_chat_messages(&data.chat_id, data.limit, data.before_message_id) + .load_prev_chat_messages(&chat_id, data.limit, data.before_message_id) .await?; data_result_ok(messages) } @@ -162,8 +166,9 @@ pub(crate) async fn load_next_message_handler( let data = data.into_inner(); data.validate()?; + let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_latest_chat_messages(&data.chat_id, data.limit, data.after_message_id) + .load_latest_chat_messages(&chat_id, data.limit, data.after_message_id) .await?; data_result_ok(messages) } @@ -175,8 +180,9 @@ pub(crate) async fn get_related_question_handler( ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; let data = data.into_inner(); + let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .get_related_questions(&data.chat_id, data.message_id) + .get_related_questions(&chat_id, data.message_id) .await?; data_result_ok(messages) } @@ -188,8 +194,9 @@ pub(crate) async fn get_answer_handler( ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; let data = data.into_inner(); + let chat_id = Uuid::from_str(&data.chat_id)?; let message = ai_manager - .generate_answer(&data.chat_id, data.message_id) + .generate_answer(&chat_id, data.message_id) .await?; data_result_ok(message) } @@ -203,7 +210,8 @@ pub(crate) async fn stop_stream_handler( data.validate()?; let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.stop_stream(&data.chat_id).await?; + let chat_id = Uuid::from_str(&data.chat_id)?; + ai_manager.stop_stream(&chat_id).await?; Ok(()) } @@ -273,7 +281,8 @@ pub(crate) async fn chat_file_handler( tracing::debug!("File size: {} bytes", file_size); let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.chat_with_file(&data.chat_id, file_path).await?; + let chat_id = Uuid::from_str(&data.chat_id)?; + ai_manager.chat_with_file(&chat_id, file_path).await?; Ok(()) } @@ -332,6 +341,7 @@ pub(crate) async fn get_chat_settings_handler( ai_manager: AFPluginState>, ) -> DataResult { let chat_id = data.try_into_inner()?.value; + let chat_id = Uuid::from_str(&chat_id)?; let ai_manager = upgrade_ai_manager(ai_manager)?; let rag_ids = ai_manager.get_rag_ids(&chat_id).await?; let pb = ChatSettingsPB { rag_ids }; @@ -345,9 +355,8 @@ pub(crate) async fn update_chat_settings_handler( ) -> FlowyResult<()> { let params = data.try_into_inner()?; let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager - .update_rag_ids(¶ms.chat_id.value, params.rag_ids) - .await?; + let chat_id = Uuid::from_str(¶ms.chat_id.value)?; + ai_manager.update_rag_ids(&chat_id, params.rag_ids).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 05f6313655..8b3285507c 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -28,6 +28,7 @@ use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; use tracing::{debug, error, info, instrument}; +use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalAISetting { @@ -51,7 +52,7 @@ const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1"; pub struct LocalAIController { ai_plugin: Arc, resource: Arc, - current_chat_id: ArcSwapOption, + current_chat_id: ArcSwapOption, store_preferences: Arc, user_service: Arc, #[allow(dead_code)] @@ -240,7 +241,7 @@ impl LocalAIController { Some(self.resource.get_llm_setting().chat_model_name) } - pub fn open_chat(&self, chat_id: &str) { + pub fn open_chat(&self, chat_id: &Uuid) { if !self.is_enabled() { return; } @@ -252,9 +253,7 @@ impl LocalAIController { self.close_chat(current_chat_id); } - self - .current_chat_id - .store(Some(Arc::new(chat_id.to_string()))); + self.current_chat_id.store(Some(Arc::new(chat_id.clone()))); let chat_id = chat_id.to_string(); let weak_ctrl = Arc::downgrade(&self.ai_plugin); tokio::spawn(async move { @@ -266,7 +265,7 @@ impl LocalAIController { }); } - pub fn close_chat(&self, chat_id: &str) { + pub fn close_chat(&self, chat_id: &Uuid) { if !self.is_running() { return; } @@ -383,7 +382,7 @@ impl LocalAIController { #[instrument(level = "debug", skip_all)] pub async fn index_message_metadata( &self, - chat_id: &str, + chat_id: &Uuid, metadata_list: &[ChatMessageMetadata], index_process_sink: &mut (impl Sink + Unpin), ) -> FlowyResult<()> { @@ -434,7 +433,7 @@ impl LocalAIController { async fn process_index_file( &self, - chat_id: &str, + chat_id: &Uuid, file_path: PathBuf, index_metadata: &HashMap, index_process_sink: &mut (impl Sink + Unpin), @@ -456,7 +455,11 @@ impl LocalAIController { let result = self .ai_plugin - .embed_file(chat_id, file_path, Some(index_metadata.clone())) + .embed_file( + &chat_id.to_string(), + file_path, + Some(index_metadata.clone()), + ) .await; match result { Ok(_) => { @@ -616,6 +619,6 @@ impl LLMResourceService for LLMResourceServiceImpl { } const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; -fn local_ai_enabled_key(workspace_id: &str) -> String { - format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) +fn local_ai_enabled_key(workspace_id: &Uuid) -> String { + format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id.to_string()) } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index dd294e6f0e..8c27268139 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -26,6 +26,7 @@ use serde_json::{json, Value}; use std::path::Path; use std::sync::{Arc, Weak}; use tracing::{info, trace}; +use uuid::Uuid; pub struct AICloudServiceMiddleware { cloud_service: Arc, @@ -55,7 +56,7 @@ impl AICloudServiceMiddleware { pub async fn index_message_metadata( &self, - chat_id: &str, + chat_id: &Uuid, metadata_list: &[ChatMessageMetadata], index_process_sink: &mut (impl Sink + Unpin), ) -> Result<(), FlowyError> { @@ -114,9 +115,9 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn create_chat( &self, uid: &i64, - workspace_id: &str, - chat_id: &str, - rag_ids: Vec, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, ) -> Result<(), FlowyError> { self .cloud_service @@ -126,8 +127,8 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn create_question( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, metadata: &[ChatMessageMetadata], @@ -140,8 +141,8 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn create_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -154,8 +155,8 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn stream_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, format: ResponseFormat, ai_model: Option, @@ -171,7 +172,12 @@ impl ChatCloudService for AICloudServiceMiddleware { let row = self.get_message_record(message_id)?; match self .local_ai - .stream_question(chat_id, &row.content, Some(json!(format)), json!({})) + .stream_question( + &chat_id.to_string(), + &row.content, + Some(json!(format)), + json!({}), + ) .await { Ok(stream) => Ok(QuestionStream::new(stream).boxed()), @@ -195,13 +201,17 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn get_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, question_message_id: i64, ) -> Result { if self.local_ai.is_running() { let content = self.get_message_record(question_message_id)?.content; - match self.local_ai.ask_question(chat_id, &content).await { + match self + .local_ai + .ask_question(&chat_id.to_string(), &content) + .await + { Ok(answer) => { let message = self .cloud_service @@ -224,8 +234,8 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn get_chat_messages( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, offset: MessageCursor, limit: u64, ) -> Result { @@ -237,26 +247,26 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn get_question_from_answer_id( &self, - workspace_id: &str, - chat_id: &str, - answer_id: i64, + workspace_id: &Uuid, + chat_id: &Uuid, + answer_message_id: i64, ) -> Result { self .cloud_service - .get_question_from_answer_id(workspace_id, chat_id, answer_id) + .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) .await } async fn get_related_message( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, ) -> Result { if self.local_ai.is_running() { let questions = self .local_ai - .get_related_question(chat_id) + .get_related_question(&chat_id.to_string()) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; trace!("LocalAI related questions: {:?}", questions); @@ -280,7 +290,7 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn stream_complete( &self, - workspace_id: &str, + workspace_id: &Uuid, params: CompleteTextParams, ai_model: Option, ) -> Result { @@ -329,15 +339,15 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn embed_file( &self, - workspace_id: &str, + workspace_id: &Uuid, file_path: &Path, - chat_id: &str, + chat_id: &Uuid, metadata: Option>, ) -> Result<(), FlowyError> { if self.local_ai.is_running() { self .local_ai - .embed_file(chat_id, file_path.to_path_buf(), metadata) + .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; Ok(()) @@ -349,21 +359,21 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { self.cloud_service.get_local_ai_config(workspace_id).await } async fn get_workspace_plan( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { self.cloud_service.get_workspace_plan(workspace_id).await } async fn get_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, ) -> Result { self .cloud_service @@ -373,8 +383,8 @@ impl ChatCloudService for AICloudServiceMiddleware { async fn update_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, params: UpdateChatParams, ) -> Result<(), FlowyError> { self @@ -383,11 +393,11 @@ impl ChatCloudService for AICloudServiceMiddleware { .await } - async fn get_available_models(&self, workspace_id: &str) -> Result { + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { self.cloud_service.get_available_models(workspace_id).await } - async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { self .cloud_service .get_workspace_default_model(workspace_id) diff --git a/frontend/rust-lib/flowy-ai/src/notification.rs b/frontend/rust-lib/flowy-ai/src/notification.rs index 97cc0e9631..6fbf3a8e7a 100644 --- a/frontend/rust-lib/flowy-ai/src/notification.rs +++ b/frontend/rust-lib/flowy-ai/src/notification.rs @@ -1,5 +1,6 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; +use tracing::trace; const CHAT_OBSERVABLE_SOURCE: &str = "Chat"; pub const APPFLOWY_AI_NOTIFICATION_KEY: &str = "appflowy_ai_plugin"; @@ -39,7 +40,12 @@ impl std::convert::From for ChatNotification { } } -#[tracing::instrument(level = "trace")] -pub(crate) fn chat_notification_builder(id: &str, ty: ChatNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, CHAT_OBSERVABLE_SOURCE) +#[tracing::instrument(level = "trace", skip_all)] +pub(crate) fn chat_notification_builder( + id: T, + ty: ChatNotification, +) -> NotificationBuilder { + let id = id.to_string(); + trace!("chat_notification_builder: id = {id}, ty = {ty:?}"); + NotificationBuilder::new(&id, ty, CHAT_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index f535dd757c..6225652078 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -32,7 +32,6 @@ collab = { workspace = true } #collab = { workspace = true, features = ["verbose_log"] } diesel.workspace = true -uuid.workspace = true flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } client-api.workspace = true @@ -56,8 +55,7 @@ lib-infra = { workspace = true } serde.workspace = true serde_json.workspace = true serde_repr.workspace = true -futures.workspace = true -walkdir = "2.4.0" +uuid.workspace = true sysinfo = "0.30.5" semver = { version = "1.0.22", features = ["serde"] } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index f9e4befeb0..5a369eb9de 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -21,6 +21,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Weak}; use tracing::{error, info}; +use uuid::Uuid; pub struct ChatDepsResolver; @@ -56,9 +57,9 @@ struct ChatQueryServiceImpl { impl AIExternalService for ChatQueryServiceImpl { async fn query_chat_rag_ids( &self, - parent_view_id: &str, - chat_id: &str, - ) -> Result, FlowyError> { + parent_view_id: &Uuid, + chat_id: &Uuid, + ) -> Result, FlowyError> { let mut ids = self .folder_service .get_surrounding_view_ids_with_view_layout(parent_view_id, ViewLayout::Document) @@ -72,9 +73,9 @@ impl AIExternalService for ChatQueryServiceImpl { } async fn sync_rag_documents( &self, - workspace_id: &str, - rag_ids: Vec, - mut rag_metadata_map: HashMap, + workspace_id: &Uuid, + rag_ids: Vec, + mut rag_metadata_map: HashMap, ) -> Result, FlowyError> { let mut result = Vec::new(); @@ -96,7 +97,7 @@ impl AIExternalService for ChatQueryServiceImpl { if let Ok(prev_sv) = StateVector::decode_v1(&metadata.prev_sync_state_vector) { let collab = Collab::new_with_source( CollabOrigin::Empty, - &rag_id, + &rag_id.to_string(), DataSource::DocStateV1(query_collab.encoded_collab.doc_state.to_vec()), vec![], false, @@ -125,7 +126,7 @@ impl AIExternalService for ChatQueryServiceImpl { } else { info!("[Chat] full sync rag document: {}", rag_id); result.push(AFCollabMetadata { - object_id: rag_id, + object_id: rag_id.to_string(), updated_at: timestamp(), prev_sync_state_vector: query_collab.encoded_collab.state_vector.to_vec(), collab_type: CollabType::Document as i32, @@ -136,7 +137,7 @@ impl AIExternalService for ChatQueryServiceImpl { Ok(result) } - async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError> { + async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError> { info!( "notify_did_send_message: chat_id: {}, message: {}", chat_id, message @@ -169,7 +170,7 @@ impl AIUserService for ChatUserServiceImpl { self.upgrade_user()?.device_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 68adc45c0e..896a03cfd5 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -7,16 +7,6 @@ use collab::core::origin::{CollabClient, CollabOrigin}; use collab::entity::EncodedCollab; use collab::preclude::CollabPlugin; use collab_entity::CollabType; -use flowy_search_pub::cloud::SearchCloudService; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; -use tokio_stream::wrappers::WatchStream; -use tracing::{debug, info}; - use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; @@ -37,12 +27,24 @@ use flowy_folder_pub::cloud::{ Workspace, WorkspaceRecord, }; use flowy_folder_pub::entities::PublishPayload; +use flowy_search_pub::cloud::SearchCloudService; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserTokenState}; use lib_infra::async_trait::async_trait; +use serde_json::Value; +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; +use tokio_stream::wrappers::WatchStream; +use tracing::log::error; +use tracing::{debug, info}; +use uuid::Uuid; use crate::server_layer::{Server, ServerProvider}; @@ -82,7 +84,7 @@ impl StorageCloudService for ServerProvider { async fn get_object_url_v1( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, ) -> FlowyResult { @@ -93,7 +95,7 @@ impl StorageCloudService for ServerProvider { .await } - async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)> { + async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { self .get_server() .ok()? @@ -104,7 +106,7 @@ impl StorageCloudService for ServerProvider { async fn create_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, content_type: &str, @@ -119,7 +121,7 @@ impl StorageCloudService for ServerProvider { async fn upload_part( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -142,7 +144,7 @@ impl StorageCloudService for ServerProvider { async fn complete_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -233,10 +235,9 @@ impl FolderCloudService for ServerProvider { server.folder_service().create_workspace(uid, &name).await } - async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); + async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { let server = self.get_server()?; - server.folder_service().open_workspace(&workspace_id).await + server.folder_service().open_workspace(workspace_id).await } async fn get_all_workspace(&self) -> Result, FlowyError> { @@ -246,7 +247,7 @@ impl FolderCloudService for ServerProvider { async fn get_folder_data( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: &i64, ) -> Result, FlowyError> { let server = self.get_server()?; @@ -272,10 +273,10 @@ impl FolderCloudService for ServerProvider { async fn get_folder_doc_state( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, collab_type: CollabType, - object_id: &str, + object_id: &Uuid, ) -> Result, FlowyError> { let server = self.get_server()?; @@ -287,7 +288,7 @@ impl FolderCloudService for ServerProvider { async fn batch_create_folder_collab_objects( &self, - workspace_id: &str, + workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -307,7 +308,7 @@ impl FolderCloudService for ServerProvider { async fn publish_view( &self, - workspace_id: &str, + workspace_id: &Uuid, payload: Vec, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -320,8 +321,8 @@ impl FolderCloudService for ServerProvider { async fn unpublish_views( &self, - workspace_id: &str, - view_ids: Vec, + workspace_id: &Uuid, + view_ids: Vec, ) -> Result<(), FlowyError> { let server = self.get_server()?; server @@ -330,15 +331,15 @@ impl FolderCloudService for ServerProvider { .await } - async fn get_publish_info(&self, view_id: &str) -> Result { + async fn get_publish_info(&self, view_id: &Uuid) -> Result { let server = self.get_server()?; server.folder_service().get_publish_info(view_id).await } async fn set_publish_name( &self, - workspace_id: &str, - view_id: String, + workspace_id: &Uuid, + view_id: Uuid, new_name: String, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -350,7 +351,7 @@ impl FolderCloudService for ServerProvider { async fn set_publish_namespace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_namespace: String, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -360,7 +361,7 @@ impl FolderCloudService for ServerProvider { .await } - async fn get_publish_namespace(&self, workspace_id: &str) -> Result { + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { let server = self.get_server()?; server .folder_service() @@ -371,7 +372,7 @@ impl FolderCloudService for ServerProvider { /// List all published views of the current workspace. async fn list_published_views( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -382,7 +383,7 @@ impl FolderCloudService for ServerProvider { async fn get_default_published_view_info( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { let server = self.get_server()?; server @@ -393,7 +394,7 @@ impl FolderCloudService for ServerProvider { async fn set_default_published_view( &self, - workspace_id: &str, + workspace_id: &Uuid, view_id: uuid::Uuid, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -403,7 +404,7 @@ impl FolderCloudService for ServerProvider { .await } - async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError> { + async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { let server = self.get_server()?; server .folder_service() @@ -421,7 +422,7 @@ impl FolderCloudService for ServerProvider { async fn full_sync_collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, params: FullSyncCollabParams, ) -> Result<(), FlowyError> { self @@ -436,24 +437,22 @@ impl FolderCloudService for ServerProvider { impl DatabaseCloudService for ServerProvider { async fn get_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { - let workspace_id = workspace_id.to_string(); let server = self.get_server()?; - let database_id = object_id.to_string(); server .database_service() - .get_database_encode_collab(&database_id, collab_type, &workspace_id) + .get_database_encode_collab(object_id, collab_type, &workspace_id) .await } async fn create_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -465,30 +464,28 @@ impl DatabaseCloudService for ServerProvider { async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { - let workspace_id = workspace_id.to_string(); let server = self.get_server()?; server .database_service() - .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) + .batch_get_database_encode_collab(object_ids, object_ty, workspace_id) .await } async fn get_database_collab_object_snapshots( &self, - object_id: &str, + object_id: &Uuid, limit: usize, ) -> Result, FlowyError> { let server = self.get_server()?; - let database_id = object_id.to_string(); server .database_service() - .get_database_collab_object_snapshots(&database_id, limit) + .get_database_collab_object_snapshots(&object_id, limit) .await } } @@ -497,29 +494,29 @@ impl DatabaseCloudService for ServerProvider { impl DatabaseAIService for ServerProvider { async fn summary_database_row( &self, - workspace_id: &str, - object_id: &str, - summary_row: SummaryRowContent, + _workspace_id: &Uuid, + _object_id: &Uuid, + _summary_row: SummaryRowContent, ) -> Result { self .get_server()? .database_ai_service() .ok_or_else(FlowyError::not_support)? - .summary_database_row(workspace_id, object_id, summary_row) + .summary_database_row(_workspace_id, _object_id, _summary_row) .await } async fn translate_database_row( &self, - workspace_id: &str, - translate_row: TranslateRowContent, - language: &str, + _workspace_id: &Uuid, + _translate_row: TranslateRowContent, + _language: &str, ) -> Result { self .get_server()? .database_ai_service() .ok_or_else(FlowyError::not_support)? - .translate_database_row(workspace_id, translate_row, language) + .translate_database_row(_workspace_id, _translate_row, _language) .await } } @@ -528,8 +525,8 @@ impl DatabaseAIService for ServerProvider { impl DocumentCloudService for ServerProvider { async fn get_document_doc_state( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -540,7 +537,7 @@ impl DocumentCloudService for ServerProvider { async fn get_document_snapshots( &self, - document_id: &str, + document_id: &Uuid, limit: usize, workspace_id: &str, ) -> Result, FlowyError> { @@ -554,8 +551,8 @@ impl DocumentCloudService for ServerProvider { async fn get_document_data( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -566,8 +563,8 @@ impl DocumentCloudService for ServerProvider { async fn create_document_collab( &self, - workspace_id: &str, - document_id: &str, + workspace_id: &Uuid, + document_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -611,26 +608,37 @@ impl CollabCloudPluginProvider for ServerProvider { collab_object.uid, collab_object.device_id.clone(), )); - let sync_object = SyncObject::new( - &collab_object.object_id, - &collab_object.workspace_id, - collab_object.collab_type, - &collab_object.device_id, - ); - let (sink, stream) = (channel.sink(), channel.stream()); - let sink_config = SinkConfig::new().send_timeout(8); - let sync_plugin = SyncPlugin::new( - origin, - sync_object, - local_collab, - sink, - sink_config, - stream, - Some(channel), - ws_connect_state, - Some(Duration::from_secs(60)), - ); - plugins.push(Box::new(sync_plugin)); + + if let (Ok(object_id), Ok(workspace_id)) = ( + Uuid::from_str(&collab_object.object_id), + Uuid::from_str(&collab_object.workspace_id), + ) { + let sync_object = SyncObject::new( + object_id, + workspace_id, + collab_object.collab_type, + &collab_object.device_id, + ); + let (sink, stream) = (channel.sink(), channel.stream()); + let sink_config = SinkConfig::new().send_timeout(8); + let sync_plugin = SyncPlugin::new( + origin, + sync_object, + local_collab, + sink, + sink_config, + stream, + Some(channel), + ws_connect_state, + Some(Duration::from_secs(60)), + ); + plugins.push(Box::new(sync_plugin)); + } else { + error!( + "Failed to parse collab object id: {}", + collab_object.object_id + ); + } }, Ok(None) => { tracing::error!("🔴Failed to get collab ws channel: channel is none"); @@ -655,9 +663,9 @@ impl ChatCloudService for ServerProvider { async fn create_chat( &self, uid: &i64, - workspace_id: &str, - chat_id: &str, - rag_ids: Vec, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, ) -> Result<(), FlowyError> { let server = self.get_server(); server? @@ -668,14 +676,12 @@ impl ChatCloudService for ServerProvider { async fn create_question( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, metadata: &[ChatMessageMetadata], ) -> Result { - let workspace_id = workspace_id.to_string(); - let chat_id = chat_id.to_string(); let message = message.to_string(); self .get_server()? @@ -686,8 +692,8 @@ impl ChatCloudService for ServerProvider { async fn create_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -701,14 +707,12 @@ impl ChatCloudService for ServerProvider { async fn stream_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result { - let workspace_id = workspace_id.to_string(); - let chat_id = chat_id.to_string(); let server = self.get_server()?; server .chat_service() @@ -718,8 +722,8 @@ impl ChatCloudService for ServerProvider { async fn get_chat_messages( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, offset: MessageCursor, limit: u64, ) -> Result { @@ -732,8 +736,8 @@ impl ChatCloudService for ServerProvider { async fn get_question_from_answer_id( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, answer_message_id: i64, ) -> Result { self @@ -745,8 +749,8 @@ impl ChatCloudService for ServerProvider { async fn get_related_message( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, ) -> Result { self @@ -758,8 +762,8 @@ impl ChatCloudService for ServerProvider { async fn get_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, question_message_id: i64, ) -> Result { let server = self.get_server(); @@ -771,23 +775,22 @@ impl ChatCloudService for ServerProvider { async fn stream_complete( &self, - workspace_id: &str, + workspace_id: &Uuid, params: CompleteTextParams, ai_model: Option, ) -> Result { - let workspace_id = workspace_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_complete(&workspace_id, params, ai_model) + .stream_complete(workspace_id, params, ai_model) .await } async fn embed_file( &self, - workspace_id: &str, + workspace_id: &Uuid, file_path: &Path, - chat_id: &str, + chat_id: &Uuid, metadata: Option>, ) -> Result<(), FlowyError> { self @@ -797,7 +800,7 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { self .get_server()? .chat_service() @@ -807,7 +810,7 @@ impl ChatCloudService for ServerProvider { async fn get_workspace_plan( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { self .get_server()? @@ -818,8 +821,8 @@ impl ChatCloudService for ServerProvider { async fn get_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, ) -> Result { self .get_server()? @@ -830,8 +833,8 @@ impl ChatCloudService for ServerProvider { async fn update_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, params: UpdateChatParams, ) -> Result<(), FlowyError> { self @@ -841,7 +844,7 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_available_models(&self, workspace_id: &str) -> Result { + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { self .get_server()? .chat_service() @@ -849,7 +852,7 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { self .get_server()? .chat_service() @@ -862,7 +865,7 @@ impl ChatCloudService for ServerProvider { impl SearchCloudService for ServerProvider { async fn document_search( &self, - workspace_id: &str, + workspace_id: &Uuid, query: String, ) -> Result, FlowyError> { let server = self.get_server()?; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index c3d0358fcb..078ee7359b 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -13,6 +13,7 @@ use collab_integrate::collab_builder::WorkspaceCollabIntegrate; use lib_infra::util::timestamp; use std::sync::{Arc, Weak}; use tracing::debug; +use uuid::Uuid; pub struct SnapshotDBImpl(pub Weak); @@ -222,12 +223,12 @@ impl WorkspaceCollabIntegrateImpl { } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { let workspace_id = self.upgrade_user()?.workspace_id()?; Ok(workspace_id) } - fn device_id(&self) -> Result { + fn device_id(&self) -> Result { Ok(self.upgrade_user()?.user_config.device_id.clone()) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 715b8207b8..1bd3223946 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -13,6 +13,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::priority_task::TaskDispatcher; use std::sync::{Arc, Weak}; use tokio::sync::RwLock; +use uuid::Uuid; pub struct DatabaseDepsResolver(); @@ -47,41 +48,41 @@ struct DatabaseAIServiceMiddleware { impl DatabaseAIService for DatabaseAIServiceMiddleware { async fn summary_database_row( &self, - workspace_id: &str, - object_id: &str, - summary_row: SummaryRowContent, + workspace_id: &Uuid, + object_id: &Uuid, + _summary_row: SummaryRowContent, ) -> Result { if self.ai_manager.local_ai.is_running() { self .ai_manager .local_ai - .summary_database_row(summary_row) + .summary_database_row(_summary_row) .await .map_err(|err| FlowyError::local_ai().with_context(err)) } else { self .ai_service - .summary_database_row(workspace_id, object_id, summary_row) + .summary_database_row(workspace_id, object_id, _summary_row) .await } } async fn translate_database_row( &self, - workspace_id: &str, - translate_row: TranslateRowContent, - language: &str, + _workspace_id: &Uuid, + _translate_row: TranslateRowContent, + _language: &str, ) -> Result { if self.ai_manager.local_ai.is_running() { let data = LocalAITranslateRowData { - cells: translate_row + cells: _translate_row .into_iter() .map(|row| LocalAITranslateItem { title: row.title, content: row.content, }) .collect(), - language: language.to_string(), + language: _language.to_string(), include_header: false, }; let resp = self @@ -95,7 +96,7 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware { } else { self .ai_service - .translate_database_row(workspace_id, translate_row, language) + .translate_database_row(_workspace_id, _translate_row, _language) .await } } @@ -121,11 +122,11 @@ impl DatabaseUser for DatabaseUserImpl { self.upgrade_user()?.get_collab_db(uid) } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } - fn workspace_database_object_id(&self) -> Result { + fn workspace_database_object_id(&self) -> Result { self.upgrade_user()?.workspace_database_object_id() } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index a4203d8268..533ac6f931 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -1,5 +1,5 @@ use std::sync::{Arc, Weak}; - +use uuid::Uuid; use crate::deps_resolve::CollabSnapshotSql; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; @@ -97,7 +97,7 @@ impl DocumentUserService for DocumentUserImpl { .device_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self .0 .upgrade() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs index f0e6985a78..bee5f19ced 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs @@ -4,6 +4,7 @@ use flowy_storage::manager::{StorageManager, StorageUserService}; use flowy_storage_pub::cloud::StorageCloudService; use flowy_user::services::authenticate_user::AuthenticateUser; use std::sync::{Arc, Weak}; +use uuid::Uuid; pub struct FileStorageResolver; @@ -40,7 +41,7 @@ impl StorageUserService for FileStorageServiceImpl { self.upgrade_user()?.user_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs index 40a7657967..585cba84c5 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs @@ -8,6 +8,7 @@ use flowy_folder::share::ImportType; use flowy_folder::view_operation::{FolderOperationHandler, ImportedData}; use lib_infra::async_trait::async_trait; use std::sync::Arc; +use uuid::Uuid; pub struct ChatFolderOperation(pub Arc); @@ -17,19 +18,19 @@ impl FolderOperationHandler for ChatFolderOperation { "ChatFolderOperationHandler" } - async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.open_chat(view_id).await } - async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.close_chat(view_id).await } - async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.delete_chat(view_id).await } - async fn duplicate_view(&self, _view_id: &str) -> Result { + async fn duplicate_view(&self, view_id: &Uuid) -> Result { Err(FlowyError::not_support()) } @@ -44,10 +45,10 @@ impl FolderOperationHandler for ChatFolderOperation { async fn create_default_view( &self, user_id: i64, - parent_view_id: &str, - view_id: &str, - _name: &str, - _layout: ViewLayout, + parent_view_id: &Uuid, + view_id: &Uuid, + name: &str, + layout: ViewLayout, ) -> Result<(), FlowyError> { self .0 @@ -58,11 +59,11 @@ impl FolderOperationHandler for ChatFolderOperation { async fn import_from_bytes( &self, - _uid: i64, - _view_id: &str, - _name: &str, - _import_type: ImportType, - _bytes: Vec, + uid: i64, + view_id: &Uuid, + name: &str, + import_type: ImportType, + bytes: Vec, ) -> Result, FlowyError> { Err(FlowyError::not_support()) } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs index d7d0c4d0cc..d57eea2ae4 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs @@ -19,23 +19,31 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; +use uuid::Uuid; pub struct DatabaseFolderOperation(pub Arc); #[async_trait] impl FolderOperationHandler for DatabaseFolderOperation { - async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.open_database_view(view_id).await?; Ok(()) } - async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { - self.0.close_database_view(view_id).await?; + async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + self + .0 + .close_database_view(view_id.to_string().as_str()) + .await?; Ok(()) } - async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { - match self.0.delete_database_view(view_id).await { + async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + match self + .0 + .delete_database_view(view_id.to_string().as_str()) + .await + { Ok(_) => tracing::trace!("Delete database view: {}", view_id), Err(e) => tracing::error!("🔴delete database failed: {}", e), } @@ -44,19 +52,23 @@ impl FolderOperationHandler for DatabaseFolderOperation { async fn gather_publish_encode_collab( &self, - user: &Arc, - view_id: &str, + _user: &Arc, + view_id: &Uuid, ) -> Result { - let workspace_id = user.workspace_id()?; + let workspace_id = _user.workspace_id()?; + let view_id_str = view_id.to_string(); // get the collab_object_id for the database. // // the collab object_id for the database is not the view_id, // we should use the view_id to get the database_id - let oid = self.0.get_database_id_with_view_id(view_id).await?; - let row_oids = self.0.get_database_row_ids_with_view_id(view_id).await?; + let oid = self.0.get_database_id_with_view_id(&view_id_str).await?; + let row_oids = self + .0 + .get_database_row_ids_with_view_id(&view_id_str) + .await?; let row_metas = self .0 - .get_database_row_metas_with_view_id(view_id, row_oids.clone()) + .get_database_row_metas_with_view_id(&view_id, row_oids.clone()) .await?; let row_document_ids = row_metas .iter() @@ -68,12 +80,12 @@ impl FolderOperationHandler for DatabaseFolderOperation { .collect::>(); let database_metas = self.0.get_all_databases_meta().await; - let uid = user + let uid = _user .user_id() .map_err(|e| e.with_context("unable to get the uid: {}"))?; // get the collab db - let collab_db = user + let collab_db = _user .collab_db(uid) .map_err(|e| e.with_context("unable to get the collab"))?; let collab_db = collab_db.upgrade().ok_or_else(|| { @@ -84,7 +96,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { tokio::task::spawn_blocking(move || { let collab_read_txn = collab_db.read_txn(); - let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, &oid) + let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), &oid) .map_err(|e| { FlowyError::internal().with_context(format!("load database collab failed: {}", e)) })?; @@ -97,7 +109,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { })?; let database_row_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_oids) + load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_oids) .0 .into_iter() .map(|(oid, collab)| { @@ -123,7 +135,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { .collect::>(); let database_row_document_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_document_ids) + load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_document_ids) .0 .into_iter() .map(|(oid, collab)| { @@ -147,7 +159,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { .await? } - async fn duplicate_view(&self, view_id: &str) -> Result { + async fn duplicate_view(&self, view_id: &Uuid) -> Result { Ok(Bytes::from(view_id.to_string())) } @@ -166,14 +178,14 @@ impl FolderOperationHandler for DatabaseFolderOperation { String::from_utf8(data.to_vec()).map_err(|_| FlowyError::invalid_data())?; let encoded_collab = self .0 - .duplicate_database(&duplicated_view_id, ¶ms.view_id) + .duplicate_database(&duplicated_view_id, ¶ms.view_id.to_string()) .await?; Ok(Some(encoded_collab)) }, ViewData::Data(data) => { let encoded_collab = self .0 - .create_database_with_data(¶ms.view_id, data.to_vec()) + .create_database_with_data(¶ms.view_id.to_string(), data.to_vec()) .await?; Ok(Some(encoded_collab)) }, @@ -212,17 +224,18 @@ impl FolderOperationHandler for DatabaseFolderOperation { /// these references views. async fn create_default_view( &self, - _user_id: i64, - _parent_view_id: &str, - view_id: &str, + user_id: i64, + parent_view_id: &Uuid, + view_id: &Uuid, name: &str, layout: ViewLayout, ) -> Result<(), FlowyError> { let name = name.to_string(); + let view_id = view_id.to_string(); let data = match layout { - ViewLayout::Grid => make_default_grid(view_id, &name), - ViewLayout::Board => make_default_board(view_id, &name), - ViewLayout::Calendar => make_default_calendar(view_id, &name), + ViewLayout::Grid => make_default_grid(&view_id, &name), + ViewLayout::Board => make_default_board(&view_id, &name), + ViewLayout::Calendar => make_default_calendar(&view_id, &name), ViewLayout::Document | ViewLayout::Chat => { return Err( FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)), @@ -244,9 +257,9 @@ impl FolderOperationHandler for DatabaseFolderOperation { async fn import_from_bytes( &self, - _uid: i64, - view_id: &str, - _name: &str, + uid: i64, + view_id: &Uuid, + name: &str, import_type: ImportType, bytes: Vec, ) -> Result, FlowyError> { diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs index af95b8987d..bb00d8c6bd 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs @@ -17,8 +17,10 @@ use flowy_folder::view_operation::{ use lib_dispatch::prelude::ToBytes; use lib_infra::async_trait::async_trait; use std::convert::TryFrom; +use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; +use uuid::Uuid; pub struct DocumentFolderOperation(pub Arc); #[async_trait] @@ -33,6 +35,7 @@ impl FolderOperationHandler for DocumentFolderOperation { workspace_view_builder: Arc>, ) -> Result<(), FlowyError> { let manager = self.0.clone(); + let mut write_guard = workspace_view_builder.write().await; // Create a view named "Getting started" with an icon ⭐️ and the built-in README data. // Don't modify this code unless you know what you are doing. @@ -45,8 +48,9 @@ impl FolderOperationHandler for DocumentFolderOperation { // create a empty document let json_str = include_str!("../../../assets/read_me.json"); let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); + let view_id = Uuid::from_str(&view.view.id).unwrap(); manager - .create_document(uid, &view.view.id, Some(document_pb.into())) + .create_document(uid, &view_id, Some(document_pb.into())) .await .unwrap(); view @@ -55,18 +59,18 @@ impl FolderOperationHandler for DocumentFolderOperation { Ok(()) } - async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.open_document(view_id).await?; Ok(()) } /// Close the document view. - async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { self.0.close_document(view_id).await?; Ok(()) } - async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { + async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { match self.0.delete_document(view_id).await { Ok(_) => tracing::trace!("Delete document: {}", view_id), Err(e) => tracing::error!("🔴delete document failed: {}", e), @@ -74,7 +78,7 @@ impl FolderOperationHandler for DocumentFolderOperation { Ok(()) } - async fn duplicate_view(&self, view_id: &str) -> Result { + async fn duplicate_view(&self, view_id: &Uuid) -> Result { let data: DocumentDataPB = self.0.get_document_data(view_id).await?.into(); let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; Ok(data_bytes) @@ -83,10 +87,11 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn gather_publish_encode_collab( &self, user: &Arc, - view_id: &str, + view_id: &Uuid, ) -> Result { let encoded_collab = - get_encoded_collab_v1_from_disk(user, view_id, CollabType::Document).await?; + get_encoded_collab_v1_from_disk(user, view_id.to_string().as_str(), CollabType::Document) + .await?; Ok(GatherEncodedCollab::Document(encoded_collab)) } @@ -112,9 +117,9 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn create_default_view( &self, user_id: i64, - _parent_view_id: &str, - view_id: &str, - _name: &str, + parent_view_id: &Uuid, + view_id: &Uuid, + name: &str, layout: ViewLayout, ) -> Result<(), FlowyError> { debug_assert_eq!(layout, ViewLayout::Document); @@ -133,9 +138,9 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn import_from_bytes( &self, uid: i64, - view_id: &str, - _name: &str, - _import_type: ImportType, + view_id: &Uuid, + name: &str, + import_type: ImportType, bytes: Vec, ) -> Result, FlowyError> { let data = DocumentDataPB::try_from(Bytes::from(bytes))?; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs index ef69fe7d82..9c064acbe3 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs @@ -17,6 +17,7 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_sqlite::kv::KVStorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::services::data_import::load_collab_by_object_id; +use std::str::FromStr; use std::sync::{Arc, Weak}; use crate::deps_resolve::folder_deps::folder_deps_chat_impl::ChatFolderOperation; @@ -25,6 +26,7 @@ use crate::deps_resolve::folder_deps::folder_deps_doc_impl::DocumentFolderOperat use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_folder_pub::query::{FolderQueryService, FolderService, FolderViewEdit, QueryCollab}; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub struct FolderDepsResolver(); #[allow(clippy::too_many_arguments)] @@ -89,7 +91,7 @@ impl FolderUser for FolderUserImpl { self.upgrade_user()?.user_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } @@ -97,8 +99,10 @@ impl FolderUser for FolderUserImpl { self.upgrade_user()?.get_collab_db(uid) } - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult { - self.upgrade_user()?.is_collab_on_disk(uid, workspace_id) + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult { + self + .upgrade_user()? + .is_collab_on_disk(uid, workspace_id.to_string().as_str()) } } @@ -124,13 +128,13 @@ impl FolderServiceImpl { #[async_trait] impl FolderViewEdit for FolderServiceImpl { - async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()> { + async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()> { if title.is_empty() { return Ok(()); } if let Some(folder_manager) = self.folder_manager.upgrade() { - if let Ok(view) = folder_manager.get_view(view_id).await { + if let Ok(view) = folder_manager.get_view(view_id.to_string().as_str()).await { if view.name.is_empty() { let title = if title.len() > 50 { title.chars().take(50).collect() @@ -160,22 +164,25 @@ impl FolderViewEdit for FolderServiceImpl { impl FolderQueryService for FolderServiceImpl { async fn get_surrounding_view_ids_with_view_layout( &self, - parent_view_id: &str, + parent_view_id: &Uuid, view_layout: ViewLayout, - ) -> Vec { + ) -> Vec { let folder_manager = match self.folder_manager.upgrade() { Some(folder_manager) => folder_manager, None => return vec![], }; - if let Ok(view) = folder_manager.get_view(parent_view_id).await { + if let Ok(view) = folder_manager + .get_view(parent_view_id.to_string().as_str()) + .await + { if view.space_info().is_some() { return vec![]; } } match folder_manager - .get_untrashed_views_belong_to(parent_view_id) + .get_untrashed_views_belong_to(parent_view_id.to_string().as_str()) .await { Ok(views) => { @@ -183,23 +190,24 @@ impl FolderQueryService for FolderServiceImpl { .into_iter() .filter_map(|child| { if child.layout == view_layout { - Some(child.id.clone()) + Uuid::from_str(&child.id).ok() } else { None } }) .collect::>(); - children.push(parent_view_id.to_string()); + children.push(parent_view_id.clone()); children }, _ => vec![], } } - async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option { - let encode_collab = get_encoded_collab_v1_from_disk(&self.user, object_id, collab_type) - .await - .ok(); + async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option { + let encode_collab = + get_encoded_collab_v1_from_disk(&self.user, object_id.to_string().as_str(), collab_type) + .await + .ok(); encode_collab.map(|encoded_collab| QueryCollab { collab_type, @@ -229,8 +237,8 @@ async fn get_encoded_collab_v1_from_disk( ) })?; let collab_read_txn = collab_db.read_txn(); - let collab = - load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, view_id).map_err(|e| { + let collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), view_id) + .map_err(|e| { FlowyError::internal().with_context(format!("load document collab failed: {}", e)) })?; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs index b6179a1ad8..1fd1d5211c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -13,6 +13,7 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; use tracing::info; +use uuid::Uuid; pub struct UserDepsResolver(); @@ -81,7 +82,7 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl { Ok(()) } - fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()> { + fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { // The remove_indices_for_workspace should not block the deletion of the workspace // Log the error and continue if let Err(err) = self diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 2c41c1f205..1562c44a22 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,20 +1,20 @@ #![allow(unused_doc_comments)] -use flowy_search::folder::indexer::FolderIndexManagerImpl; -use flowy_search::services::manager::SearchManager; -use std::sync::{Arc, Weak}; -use std::time::Duration; -use sysinfo::System; -use tokio::sync::RwLock; -use tracing::{debug, error, event, info, instrument}; - use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::FolderManager; +use flowy_search::folder::indexer::FolderIndexManagerImpl; +use flowy_search::services::manager::SearchManager; use flowy_server::af_cloud::define::ServerUser; +use std::sync::{Arc, Weak}; +use std::time::Duration; +use sysinfo::System; +use tokio::sync::RwLock; +use tracing::{debug, error, event, info, instrument}; +use uuid::Uuid; use flowy_sqlite::kv::KVStorePreferences; use flowy_storage::manager::StorageManager; @@ -329,7 +329,7 @@ impl ServerUserImpl { } } impl ServerUser for ServerUserImpl { - fn workspace_id(&self) -> FlowyResult { + fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 5a6a2e7b2a..2882b9b050 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -55,6 +55,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { _device_id: &str, authenticator: &Authenticator, ) -> FlowyResult<()> { + let workspace_id = user_workspace.workspace_id()?; self .server_provider .set_user_authenticator(user_authenticator); @@ -74,7 +75,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .folder_manager .initialize( user_id, - &user_workspace.id, + &workspace_id, FolderInitDataSource::LocalDisk { create_if_not_exist: false, }, @@ -140,6 +141,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); + let workspace_id = user_workspace.workspace_id()?; // In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace // is automatically created for them. However, for users who sign up through Supabase, the creation @@ -149,10 +151,10 @@ impl UserStatusCallback for UserStatusCallbackImpl { .folder_manager .cloud_service .get_folder_doc_state( - &user_workspace.id, + &workspace_id, user_profile.uid, CollabType::Folder, - &user_workspace.id, + &workspace_id, ) .await { @@ -179,7 +181,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { &user_profile.token, is_new_user, data_source, - &user_workspace.id, + &workspace_id, ) .await .context("FolderManager error")?; diff --git a/frontend/rust-lib/flowy-database-pub/Cargo.toml b/frontend/rust-lib/flowy-database-pub/Cargo.toml index 91426a5c87..088c7b6465 100644 --- a/frontend/rust-lib/flowy-database-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-database-pub/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" lib-infra = { workspace = true } collab-entity = { workspace = true } collab = { workspace = true } -anyhow.workspace = true client-api = { workspace = true } -flowy-error = { workspace = true } \ No newline at end of file +flowy-error = { workspace = true } +uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index a29cf650c4..8666e6c764 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -4,8 +4,9 @@ use collab_entity::CollabType; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use std::collections::HashMap; +use uuid::Uuid; -pub type EncodeCollabByOid = HashMap; +pub type EncodeCollabByOid = HashMap; pub type SummaryRowContent = HashMap; pub type TranslateRowContent = Vec; @@ -13,8 +14,8 @@ pub type TranslateRowContent = Vec; pub trait DatabaseAIService: Send + Sync { async fn summary_database_row( &self, - _workspace_id: &str, - _object_id: &str, + _workspace_id: &Uuid, + _object_id: &Uuid, _summary_row: SummaryRowContent, ) -> Result { Ok("".to_string()) @@ -22,7 +23,7 @@ pub trait DatabaseAIService: Send + Sync { async fn translate_database_row( &self, - _workspace_id: &str, + _workspace_id: &Uuid, _translate_row: TranslateRowContent, _language: &str, ) -> Result { @@ -41,29 +42,29 @@ pub trait DatabaseAIService: Send + Sync { pub trait DatabaseCloudService: Send + Sync { async fn get_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError>; async fn create_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError>; async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result; async fn get_database_collab_object_snapshots( &self, - object_id: &str, + object_id: &Uuid, limit: usize, ) -> Result, FlowyError>; } diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index d7bee42420..ec0eb94210 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -51,6 +51,7 @@ strum_macros = "0.25" validator = { workspace = true, features = ["derive"] } tokio-util.workspace = true moka = { version = "0.12.8", features = ["future"] } +uuid.workspace = true [dev-dependencies] event-integration-test = { path = "../event-integration-test", default-features = false } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index ed766885c7..6117a8ee8a 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1261,7 +1261,7 @@ pub(crate) async fn summarize_row_handler( let (tx, rx) = oneshot::channel(); tokio::spawn(async move { let result = manager - .summarize_row(data.view_id, row_id, data.field_id) + .summarize_row(&data.view_id, row_id, data.field_id) .await; let _ = tx.send(result); }); @@ -1280,7 +1280,7 @@ pub(crate) async fn translate_row_handler( let (tx, rx) = oneshot::channel(); tokio::spawn(async move { let result = manager - .translate_row(data.view_id, row_id, data.field_id) + .translate_row(&data.view_id, row_id, data.field_id) .await; let _ = tx.send(result); }); diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 63f95011bd..076b77e1d0 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -20,6 +20,7 @@ use collab_entity::{CollabObject, CollabType, EncodedCollab}; use collab_plugins::local_storage::kv::KVTransactionDB; use rayon::prelude::*; use std::collections::HashMap; +use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; use tokio::sync::Mutex; @@ -42,12 +43,13 @@ use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::field_settings::default_field_settings_by_layout_map; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; use tokio::sync::RwLock as TokioRwLock; +use uuid::Uuid; pub trait DatabaseUser: Send + Sync { fn user_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; - fn workspace_id(&self) -> Result; - fn workspace_database_object_id(&self) -> Result; + fn workspace_id(&self) -> Result; + fn workspace_database_object_id(&self) -> Result; } pub(crate) type DatabaseEditorMap = HashMap>; @@ -110,7 +112,7 @@ impl DatabaseManager { let workspace_database_object_id = self.user.workspace_database_object_id()?; let workspace_database_collab = collab_service .build_collab( - workspace_database_object_id.as_str(), + workspace_database_object_id.to_string().as_str(), CollabType::WorkspaceDatabase, None, ) @@ -189,8 +191,10 @@ impl DatabaseManager { }) } - pub async fn encode_database(&self, view_id: &str) -> FlowyResult { - let editor = self.get_database_editor_with_view_id(view_id).await?; + pub async fn encode_database(&self, view_id: &Uuid) -> FlowyResult { + let editor = self + .get_database_editor_with_view_id(view_id.to_string().as_str()) + .await?; let collabs = editor .database .read() @@ -207,10 +211,12 @@ impl DatabaseManager { pub async fn get_database_row_metas_with_view_id( &self, - view_id: &str, + view_id: &Uuid, row_ids: Vec, ) -> FlowyResult> { - let database = self.get_database_editor_with_view_id(view_id).await?; + let database = self + .get_database_editor_with_view_id(view_id.to_string().as_str()) + .await?; let view_id = view_id.to_string(); let mut row_metas: Vec = vec![]; for row_id in row_ids { @@ -275,11 +281,11 @@ impl DatabaseManager { /// Open the database view #[instrument(level = "trace", skip_all, err)] - pub async fn open_database_view>(&self, view_id: T) -> FlowyResult<()> { - let view_id = view_id.as_ref(); + pub async fn open_database_view(&self, view_id: &Uuid) -> FlowyResult<()> { + let view_id = view_id.to_string(); let lock = self.workspace_database()?; let workspace_database = lock.read().await; - let result = workspace_database.get_database_id_with_view_id(view_id); + let result = workspace_database.get_database_id_with_view_id(&view_id); drop(workspace_database); if let Some(database_id) = result { @@ -292,8 +298,7 @@ impl DatabaseManager { } #[instrument(level = "trace", skip_all, err)] - pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { - let view_id = view_id.as_ref(); + pub async fn close_database_view(&self, view_id: &str) -> FlowyResult<()> { let lock = self.workspace_database()?; let workspace_database = lock.read().await; let database_id = workspace_database.get_database_id_with_view_id(view_id); @@ -518,7 +523,9 @@ impl DatabaseManager { layout: DatabaseLayoutPB, ) -> FlowyResult<()> { let database = self.get_database_editor_with_view_id(view_id).await?; - database.update_view_layout(view_id, layout.into()).await + database + .update_view_layout(view_id.to_string().as_str(), layout.into()) + .await } pub async fn get_database_snapshots( @@ -526,7 +533,7 @@ impl DatabaseManager { view_id: &str, limit: usize, ) -> FlowyResult> { - let database_id = self.get_database_id_with_view_id(view_id).await?; + let database_id = Uuid::from_str(&self.get_database_id_with_view_id(view_id).await?)?; let snapshots = self .cloud_service .get_database_collab_object_snapshots(&database_id, limit) @@ -553,11 +560,11 @@ impl DatabaseManager { #[instrument(level = "debug", skip_all)] pub async fn summarize_row( &self, - view_id: String, + view_id: &str, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(&view_id).await?; + let database = self.get_database_editor_with_view_id(view_id).await?; let mut summary_row_content = SummaryRowContent::new(); if let Some(row) = database.get_row(&view_id, &row_id).await { let fields = database.get_fields(&view_id, None).await; @@ -583,7 +590,11 @@ impl DatabaseManager { ); let response = self .ai_service - .summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content) + .summary_database_row( + &self.user.workspace_id()?, + &Uuid::from_str(&row_id)?, + summary_row_content, + ) .await?; trace!("[AI]:summarize row response: {}", response); @@ -597,11 +608,12 @@ impl DatabaseManager { #[instrument(level = "debug", skip_all)] pub async fn translate_row( &self, - view_id: String, + view_id: &str, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(&view_id).await?; + let database = self.get_database_editor_with_view_id(view_id).await?; + let view_id = view_id.to_string(); let mut translate_row_content = TranslateRowContent::new(); let mut language = "english".to_string(); @@ -703,10 +715,13 @@ impl WorkspaceDatabaseCollabServiceImpl { async fn get_encode_collab( &self, - object_id: &str, + object_id: &Uuid, object_ty: CollabType, ) -> Result, DatabaseError> { - let workspace_id = self.user.workspace_id().unwrap(); + let workspace_id = self + .user + .workspace_id() + .map_err(|e| DatabaseError::Internal(e.into()))?; trace!("[Database]: fetch {}:{} from remote", object_id, object_ty); let encode_collab = self .cloud_service @@ -718,7 +733,7 @@ impl WorkspaceDatabaseCollabServiceImpl { async fn batch_get_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, ) -> Result { let workspace_id = self @@ -730,7 +745,13 @@ impl WorkspaceDatabaseCollabServiceImpl { .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) .await .map_err(|err| DatabaseError::Internal(err.into()))?; - Ok(updates) + + Ok( + updates + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + ) } fn collab_db(&self) -> Result, DatabaseError> { @@ -746,7 +767,7 @@ impl WorkspaceDatabaseCollabServiceImpl { fn build_collab_object( &self, - object_id: &str, + object_id: &Uuid, object_type: CollabType, ) -> Result { let uid = self @@ -776,8 +797,12 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { collab_type: CollabType, encoded_collab: Option<(EncodedCollab, bool)>, ) -> Result { - let object = self.build_collab_object(object_id, collab_type)?; - let data_source = if self.persistence.is_collab_exist(object_id) { + let object_id = Uuid::parse_str(object_id)?; + let object = self.build_collab_object(&object_id, collab_type)?; + let data_source = if self + .persistence + .is_collab_exist(object_id.to_string().as_str()) + { trace!( "build collab: {}:{} from local encode collab", collab_type, @@ -796,7 +821,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { object_id, encoded_collab.is_none(), ); - match self.get_encode_collab(object_id, collab_type).await { + match self.get_encode_collab(&object_id, collab_type).await { Ok(Some(encode_collab)) => { info!( "build collab: {}:{} with remote encode collab, {} bytes", @@ -837,12 +862,11 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { ); self .persistence - .save_collab(object_id, encoded_collab.clone())?; + .save_collab(object_id.to_string().as_str(), encoded_collab.clone())?; // TODO(nathan): cover database rows and other database collab type if matches!(collab_type, CollabType::Database) { if let Ok(workspace_id) = self.user.workspace_id() { - let object_id = object_id.to_string(); let cloned_encoded_collab = encoded_collab.clone(); let cloud_service = self.cloud_service.clone(); tokio::spawn(async move { @@ -878,6 +902,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { if object_ids.is_empty() { return Ok(EncodeCollabByOid::new()); } + let mut encoded_collab_by_id = EncodeCollabByOid::new(); // 1. Collect local disk collabs into a HashMap let local_disk_encoded_collab: HashMap = object_ids @@ -900,6 +925,10 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { } if !object_ids.is_empty() { + let object_ids = object_ids + .into_iter() + .flat_map(|v| Uuid::from_str(&v).ok()) + .collect::>(); // 2. Fetch remaining collabs from remote let remote_collabs = self .batch_get_encode_collab(object_ids, collab_type) @@ -927,7 +956,7 @@ pub struct DatabasePersistenceImpl { } impl DatabasePersistenceImpl { - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { let workspace_id = self .user .workspace_id() @@ -947,7 +976,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { if let Ok((uid, Ok(Some(collab_db)))) = result { let object_id = collab.object_id().to_string(); let db_read = collab_db.read_txn(); - if !db_read.is_exist(uid, &workspace_id, &object_id) { + if !db_read.is_exist(uid, workspace_id.to_string().as_str(), &object_id) { trace!( "[Database]: collab:{} not exist in local storage", object_id @@ -957,7 +986,12 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { trace!("[Database]: start loading collab:{} from disk", object_id); let mut txn = collab.transact_mut(); - match db_read.load_doc_with_txn(uid, &workspace_id, &object_id, &mut txn) { + match db_read.load_doc_with_txn( + uid, + workspace_id.to_string().as_str(), + &object_id, + &mut txn, + ) { Ok(update_count) => { trace!( "[Database]: did load collab:{}, update_count:{}", @@ -976,7 +1010,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { } fn get_encoded_collab(&self, object_id: &str, collab_type: CollabType) -> Option { - let workspace_id = self.user.workspace_id().ok()?; + let workspace_id = self.user.workspace_id().ok()?.to_string(); let uid = self.user.user_id().ok()?; let db = self.user.collab_db(uid).ok()?.upgrade()?; let read_txn = db.read_txn(); @@ -995,7 +1029,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { } fn delete_collab(&self, object_id: &str) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?; + let workspace_id = self.workspace_id()?.to_string(); let uid = self .user .user_id() @@ -1017,7 +1051,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { object_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?; + let workspace_id = self.workspace_id()?.to_string(); let uid = self .user .user_id() @@ -1051,7 +1085,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { Ok(uid) => { if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { let read_txn = collab_db.read_txn(); - return read_txn.is_exist(uid, workspace_id.as_str(), object_id); + return read_txn.is_exist(uid, workspace_id.to_string().as_str(), object_id); } false }, @@ -1073,7 +1107,8 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { let workspace_id = self .user .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; + .map_err(|err| DatabaseError::Internal(err.into()))? + .to_string(); if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { let write_txn = collab_db.write_txn(); diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index e284466054..ace2ec52fc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -44,6 +44,7 @@ use lib_infra::box_any::BoxAny; use lib_infra::priority_task::TaskDispatcher; use lib_infra::util::timestamp; use std::collections::HashMap; +use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; use tokio::select; @@ -53,11 +54,12 @@ use tokio::sync::{broadcast, oneshot}; use tokio::task::yield_now; use tokio_util::sync::CancellationToken; use tracing::{debug, error, event, info, instrument, trace, warn}; +use uuid::Uuid; type OpenDatabaseResult = oneshot::Sender>; pub struct DatabaseEditor { - database_id: String, + database_id: Uuid, pub(crate) database: Arc>, pub cell_cache: CellCache, pub(crate) database_views: Arc, @@ -117,6 +119,7 @@ impl DatabaseEditor { .await?, ); + let database_id = Uuid::from_str(&database_id)?; let collab_object = collab_builder.collab_object( &user.workspace_id()?, user.user_id()?, @@ -806,10 +809,11 @@ impl DatabaseEditor { let is_finalized = self.finalized_rows.get(row_id.as_str()).await.is_some(); if !is_finalized { trace!("[Database]: finalize database row: {}", row_id); + let row_id = Uuid::from_str(row_id.as_str())?; let collab_object = self.collab_builder.collab_object( &self.user.workspace_id()?, self.user.user_id()?, - row_id, + &row_id, CollabType::DatabaseRow, )?; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index 081d23f1b3..1c965995ec 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -16,6 +16,7 @@ use futures::StreamExt; use std::sync::Arc; use tracing::{error, trace, warn}; +use uuid::Uuid; pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc>) { let weak_database = Arc::downgrade(database); @@ -112,7 +113,7 @@ pub(crate) async fn observe_field_change(database_id: &str, database: &Arc) { +pub(crate) async fn observe_view_change(database_id: &Uuid, database_editor: &Arc) { let database_id = database_id.to_string(); let weak_database_editor = Arc::downgrade(database_editor); let view_change = database_editor @@ -289,7 +290,7 @@ async fn handle_did_update_row_orders( } } -pub(crate) async fn observe_block_event(database_id: &str, database_editor: &Arc) { +pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Arc) { let database_id = database_id.to_string(); let mut block_event_rx = database_editor .database diff --git a/frontend/rust-lib/flowy-document-pub/Cargo.toml b/frontend/rust-lib/flowy-document-pub/Cargo.toml index 93a282f5cc..cbb74de5c4 100644 --- a/frontend/rust-lib/flowy-document-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-document-pub/Cargo.toml @@ -9,5 +9,5 @@ edition = "2021" lib-infra = { workspace = true } flowy-error = { workspace = true } collab-document = { workspace = true } -anyhow.workspace = true -collab = { workspace = true } \ No newline at end of file +collab = { workspace = true } +uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document-pub/src/cloud.rs b/frontend/rust-lib/flowy-document-pub/src/cloud.rs index f34a91bfd4..d5c25053a8 100644 --- a/frontend/rust-lib/flowy-document-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-pub/src/cloud.rs @@ -1,8 +1,8 @@ use collab::entity::EncodedCollab; pub use collab_document::blocks::DocumentData; - use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use uuid::Uuid; /// A trait for document cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of @@ -11,27 +11,27 @@ use lib_infra::async_trait::async_trait; pub trait DocumentCloudService: Send + Sync + 'static { async fn get_document_doc_state( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError>; async fn get_document_snapshots( &self, - document_id: &str, + document_id: &Uuid, limit: usize, workspace_id: &str, ) -> Result, FlowyError>; async fn get_document_data( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError>; async fn create_document_collab( &self, - workspace_id: &str, - document_id: &str, + workspace_id: &Uuid, + document_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError>; } diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index ffdd0c900e..aa871cf4bc 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -6,9 +6,10 @@ use collab::preclude::Collab; use collab_document::document::Document; use futures::StreamExt; use lib_infra::sync_trace; +use uuid::Uuid; -pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) { - let doc_id_clone_for_block_changed = doc_id.to_owned(); +pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) { + let doc_id_clone_for_block_changed = doc_id.to_string(); document.subscribe_block_changed("key", move |events, is_remote| { sync_trace!( "[Document] block changed in doc_id: {}, is_remote: {}, events: {:?}", @@ -35,7 +36,7 @@ pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) { ); document_notification_builder( - &doc_id_clone_for_awareness_state, + &doc_id_clone_for_awareness_state.to_string(), DocumentNotification::DidUpdateDocumentAwarenessState, ) .payload::(events.into()) diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index 74157c6124..c8a6765fd6 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use collab::core::collab_state::SyncState; use collab_document::{ blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData}, @@ -8,10 +6,12 @@ use collab_document::{ DocumentAwarenessUser, }, }; - use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use lib_infra::validator_fn::{required_not_empty_str, required_valid_path}; +use std::collections::HashMap; +use std::str::FromStr; +use uuid::Uuid; use validator::Validate; use crate::parse::{NotEmptyStr, NotEmptyVec}; @@ -31,7 +31,7 @@ pub struct OpenDocumentPayloadPB { } pub struct OpenDocumentParams { - pub document_id: String, + pub document_id: Uuid, } impl TryInto for OpenDocumentPayloadPB { @@ -39,9 +39,9 @@ impl TryInto for OpenDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - Ok(OpenDocumentParams { - document_id: document_id.0, - }) + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; + + Ok(OpenDocumentParams { document_id }) } } @@ -52,7 +52,7 @@ pub struct DocumentRedoUndoPayloadPB { } pub struct DocumentRedoUndoParams { - pub document_id: String, + pub document_id: Uuid, } impl TryInto for DocumentRedoUndoPayloadPB { @@ -60,9 +60,8 @@ impl TryInto for DocumentRedoUndoPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - Ok(DocumentRedoUndoParams { - document_id: document_id.0, - }) + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; + Ok(DocumentRedoUndoParams { document_id }) } } @@ -132,7 +131,7 @@ pub struct CreateDocumentPayloadPB { } pub struct CreateDocumentParams { - pub document_id: String, + pub document_id: Uuid, pub initial_data: Option, } @@ -141,9 +140,10 @@ impl TryInto for CreateDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let initial_data = self.initial_data.map(|data| data.into()); Ok(CreateDocumentParams { - document_id: document_id.0, + document_id, initial_data, }) } @@ -156,7 +156,7 @@ pub struct CloseDocumentPayloadPB { } pub struct CloseDocumentParams { - pub document_id: String, + pub document_id: Uuid, } impl TryInto for CloseDocumentPayloadPB { @@ -164,9 +164,8 @@ impl TryInto for CloseDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - Ok(CloseDocumentParams { - document_id: document_id.0, - }) + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; + Ok(CloseDocumentParams { document_id }) } } @@ -180,7 +179,7 @@ pub struct ApplyActionPayloadPB { } pub struct ApplyActionParams { - pub document_id: String, + pub document_id: Uuid, pub actions: Vec, } @@ -189,10 +188,11 @@ impl TryInto for ApplyActionPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?; let actions = actions.0.into_iter().map(BlockAction::from).collect(); Ok(ApplyActionParams { - document_id: document_id.0, + document_id, actions, }) } @@ -525,7 +525,7 @@ pub struct TextDeltaPayloadPB { } pub struct TextDeltaParams { - pub document_id: String, + pub document_id: Uuid, pub text_id: String, pub delta: String, } @@ -535,10 +535,11 @@ impl TryInto for TextDeltaPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let text_id = NotEmptyStr::parse(self.text_id).map_err(|_| ErrorCode::TextIdIsEmpty)?; let delta = self.delta.map_or_else(|| "".to_string(), |delta| delta); Ok(TextDeltaParams { - document_id: document_id.0, + document_id, text_id: text_id.0, delta, }) diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 387e216f08..cb16129c40 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -3,7 +3,7 @@ * as well as performing actions on documents. These functions make use of a DocumentManager, * which you can think of as a higher-level interface to interact with documents. */ - +use std::str::FromStr; use std::sync::{Arc, Weak}; use collab_document::blocks::{ @@ -11,10 +11,6 @@ use collab_document::blocks::{ DocumentData, }; -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use tracing::instrument; - use crate::entities::*; use crate::parser::document_data_parser::DocumentDataParser; use crate::parser::external::parser::ExternalDataToNestedJSONParser; @@ -23,6 +19,10 @@ use crate::parser::parser_entities::{ ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, }; use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use tracing::instrument; +use uuid::Uuid; fn upgrade_document( document_manager: AFPluginState>, @@ -499,7 +499,7 @@ pub(crate) async fn set_awareness_local_state_handler( ) -> FlowyResult<()> { let manager = upgrade_document(manager)?; let data = data.into_inner(); - let doc_id = data.document_id.clone(); + let doc_id = Uuid::from_str(&data.document_id)?; manager .set_document_awareness_local_state(&doc_id, data) .await?; diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index b84469872b..9e78b02f9b 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -14,21 +14,21 @@ use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; -use collab_plugins::CollabKVDB; -use dashmap::DashMap; -use lib_infra::util::timestamp; -use tracing::{event, instrument}; -use tracing::{info, trace}; - use crate::document::{ subscribe_document_changed, subscribe_document_snapshot_state, subscribe_document_sync_state, }; use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, }; +use collab_plugins::CollabKVDB; +use dashmap::DashMap; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::storage::{CreatedUpload, StorageService}; +use lib_infra::util::timestamp; +use tracing::{event, instrument}; +use tracing::{info, trace}; +use uuid::Uuid; use crate::entities::UpdateDocumentAwarenessStatePB; use crate::entities::{ @@ -39,7 +39,7 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUserService: Send + Sync { fn user_id(&self) -> Result; fn device_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; } @@ -54,8 +54,8 @@ pub trait DocumentSnapshotService: Send + Sync { pub struct DocumentManager { pub user_service: Arc, collab_builder: Arc, - documents: Arc>>>, - removing_documents: Arc>>>, + documents: Arc>>>, + removing_documents: Arc>>>, cloud_service: Arc, storage_service: Weak, snapshot_service: Arc, @@ -81,7 +81,7 @@ impl DocumentManager { } /// Get the encoded collab of the document. - pub async fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult { + pub async fn get_encoded_collab_with_view_id(&self, doc_id: &Uuid) -> FlowyResult { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; let doc_state = @@ -139,7 +139,7 @@ impl DocumentManager { pub async fn create_document( &self, _uid: i64, - doc_id: &str, + doc_id: &Uuid, data: Option, ) -> FlowyResult { if self.is_doc_exist(doc_id).await.unwrap_or(false) { @@ -151,17 +151,17 @@ impl DocumentManager { let encoded_collab = doc_state_from_document_data(doc_id, data).await?; self .persistence()? - .save_collab_to_disk(doc_id, encoded_collab.clone()) + .save_collab_to_disk(doc_id.to_string().as_str(), encoded_collab.clone()) .map_err(internal_error)?; // Send the collab data to server with a background task. let cloud_service = self.cloud_service.clone(); let cloned_encoded_collab = encoded_collab.clone(); - let document_id = doc_id.to_string(); let workspace_id = self.user_service.workspace_id()?; + let doc_id = doc_id.clone(); tokio::spawn(async move { let _ = cloud_service - .create_document_collab(&workspace_id, &document_id, cloned_encoded_collab) + .create_document_collab(&workspace_id, &doc_id, cloned_encoded_collab) .await; }); Ok(encoded_collab) @@ -171,7 +171,7 @@ impl DocumentManager { async fn collab_for_document( &self, uid: i64, - doc_id: &str, + doc_id: &Uuid, data_source: DataSource, sync_enable: bool, ) -> FlowyResult>> { @@ -195,7 +195,7 @@ impl DocumentManager { } /// Return a document instance if the document is already opened. - pub async fn editable_document(&self, doc_id: &str) -> FlowyResult>> { + pub async fn editable_document(&self, doc_id: &Uuid) -> FlowyResult>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -213,7 +213,7 @@ impl DocumentManager { #[tracing::instrument(level = "info", skip(self), err)] async fn create_document_instance( &self, - doc_id: &str, + doc_id: &Uuid, enable_sync: bool, ) -> FlowyResult>> { let uid = self.user_service.user_id()?; @@ -260,7 +260,7 @@ impl DocumentManager { subscribe_document_snapshot_state(&lock); subscribe_document_sync_state(&lock); } - self.documents.insert(doc_id.to_string(), document.clone()); + self.documents.insert(doc_id.clone(), document.clone()); } Ok(document) }, @@ -273,21 +273,21 @@ impl DocumentManager { } } - pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { + pub async fn get_document_data(&self, doc_id: &Uuid) -> FlowyResult { let document = self.get_document(doc_id).await?; let document = document.read().await; document.get_document_data().map_err(internal_error) } - pub async fn get_document_text(&self, doc_id: &str) -> FlowyResult { + pub async fn get_document_text(&self, doc_id: &Uuid) -> FlowyResult { let document = self.get_document(doc_id).await?; let document = document.read().await; - let text = document.to_plain_text(true, false)?; + let text = document.paragraphs().join("\n"); Ok(text) } /// Return a document instance. /// The returned document might or might not be able to sync with the cloud. - async fn get_document(&self, doc_id: &str) -> FlowyResult>> { + async fn get_document(&self, doc_id: &Uuid) -> FlowyResult>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -300,7 +300,7 @@ impl DocumentManager { Ok(document) } - pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> { + pub async fn open_document(&self, doc_id: &Uuid) -> FlowyResult<()> { if let Some(mutex_document) = self.restore_document_from_removing(doc_id) { let lock = mutex_document.read().await; lock.start_init_sync(); @@ -314,7 +314,7 @@ impl DocumentManager { Ok(()) } - pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> { + pub async fn close_document(&self, doc_id: &Uuid) -> FlowyResult<()> { if let Some((doc_id, document)) = self.documents.remove(doc_id) { { // clear the awareness state when close the document @@ -340,11 +340,12 @@ impl DocumentManager { Ok(()) } - pub async fn delete_document(&self, doc_id: &str) -> FlowyResult<()> { + pub async fn delete_document(&self, doc_id: &Uuid) -> FlowyResult<()> { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; if let Some(db) = self.user_service.collab_db(uid)?.upgrade() { - db.delete_doc(uid, &workspace_id, doc_id).await?; + db.delete_doc(uid, &workspace_id.to_string(), &doc_id.to_string()) + .await?; // When deleting a document, we need to remove it from the cache. self.documents.remove(doc_id); } @@ -354,7 +355,7 @@ impl DocumentManager { #[instrument(level = "debug", skip_all, err)] pub async fn set_document_awareness_local_state( &self, - doc_id: &str, + doc_id: &Uuid, state: UpdateDocumentAwarenessStatePB, ) -> FlowyResult { let uid = self.user_service.user_id()?; @@ -379,12 +380,12 @@ impl DocumentManager { /// Return the list of snapshots of the document. pub async fn get_document_snapshot_meta( &self, - document_id: &str, + document_id: &Uuid, _limit: usize, ) -> FlowyResult> { let metas = self .snapshot_service - .get_document_snapshot_metas(document_id)? + .get_document_snapshot_metas(document_id.to_string().as_str())? .into_iter() .map(|meta| DocumentSnapshotMetaPB { snapshot_id: meta.snapshot_id, @@ -434,11 +435,13 @@ impl DocumentManager { Ok(()) } - async fn is_doc_exist(&self, doc_id: &str) -> FlowyResult { + async fn is_doc_exist(&self, doc_id: &Uuid) -> FlowyResult { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() { - let is_exist = collab_db.is_exist(uid, &workspace_id, doc_id).await?; + let is_exist = collab_db + .is_exist(uid, &workspace_id.to_string(), &doc_id.to_string()) + .await?; Ok(is_exist) } else { Ok(false) @@ -463,7 +466,7 @@ impl DocumentManager { &self.storage_service } - fn restore_document_from_removing(&self, doc_id: &str) -> Option>> { + fn restore_document_from_removing(&self, doc_id: &Uuid) -> Option>> { let (doc_id, doc) = self.removing_documents.remove(doc_id)?; trace!( "move document {} from removing_documents to documents", @@ -475,7 +478,7 @@ impl DocumentManager { } async fn doc_state_from_document_data( - doc_id: &str, + doc_id: &Uuid, data: Option, ) -> Result { let doc_id = doc_id.to_string(); diff --git a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs index 8acdecae36..94680b32d3 100644 --- a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs +++ b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; +use uuid::Uuid; use validator::Validate; #[derive(Default, ProtoBuf)] @@ -96,7 +97,7 @@ pub struct ParseType { } pub struct ConvertDocumentParams { - pub document_id: String, + pub document_id: Uuid, pub range: Option, pub parse_types: ParseType, } @@ -140,10 +141,11 @@ impl TryInto for ConvertDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let document_id = Uuid::parse_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let range = self.range.map(|data| data.into()); Ok(ConvertDocumentParams { - document_id: document_id.0, + document_id, range, parse_types: self.parse_types.into(), }) diff --git a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs index b11cd2ecde..2a47ec93c4 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs @@ -9,8 +9,8 @@ use crate::document::util::{gen_document_id, gen_id, DocumentTest}; async fn undo_redo_test() { let test = DocumentTest::new(); - let doc_id: String = gen_document_id(); - let data = default_document_data(&doc_id); + let doc_id = gen_document_id(); + let data = default_document_data(&doc_id.to_string()); // create a document _ = test diff --git a/frontend/rust-lib/flowy-document/tests/document/document_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_test.rs index d7906bc114..8323a645c7 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_test.rs @@ -11,8 +11,8 @@ async fn restore_document() { let test = DocumentTest::new(); // create a document - let doc_id: String = gen_document_id(); - let data = default_document_data(&doc_id); + let doc_id = gen_document_id(); + let data = default_document_data(&doc_id.to_string()); let uid = test.user_service.user_id().unwrap(); test .create_document(uid, &doc_id, Some(data.clone())) @@ -55,8 +55,8 @@ async fn restore_document() { async fn document_apply_insert_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id: String = gen_document_id(); - let data = default_document_data(&doc_id); + let doc_id = gen_document_id(); + let data = default_document_data(&doc_id.to_string()); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; @@ -111,9 +111,9 @@ async fn document_apply_insert_action() { #[tokio::test] async fn document_apply_update_page_action() { let test = DocumentTest::new(); - let doc_id: String = gen_document_id(); + let doc_id = gen_document_id(); let uid = test.user_service.user_id().unwrap(); - let data = default_document_data(&doc_id); + let data = default_document_data(&doc_id.to_string()); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; @@ -158,8 +158,8 @@ async fn document_apply_update_page_action() { async fn document_apply_update_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id: String = gen_document_id(); - let data = default_document_data(&doc_id); + let doc_id = gen_document_id(); + let data = default_document_data(&doc_id.to_string()); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 20e6b5d79d..a7ae759709 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -7,11 +7,6 @@ use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; use collab_document::document::Document; use collab_document::document_data::default_document_data; -use nanoid::nanoid; -use tempfile::TempDir; -use tokio::sync::RwLock; -use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; - use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, WorkspaceCollabIntegrate, @@ -24,6 +19,11 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::storage::{CreatedUpload, FileProgressReceiver, StorageService}; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; +use nanoid::nanoid; +use tempfile::TempDir; +use tokio::sync::RwLock; +use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; +use uuid::Uuid; pub struct DocumentTest { inner: DocumentManager, @@ -63,7 +63,7 @@ impl Deref for DocumentTest { } pub struct FakeUser { - workspace_id: String, + workspace_id: Uuid, collab_db: Arc, } @@ -74,7 +74,7 @@ impl FakeUser { let tempdir = TempDir::new().unwrap(); let path = tempdir.into_path(); let collab_db = Arc::new(CollabKVDB::open(path).unwrap()); - let workspace_id = uuid::Uuid::new_v4().to_string(); + let workspace_id = uuid::Uuid::new_v4(); Self { collab_db, @@ -88,7 +88,7 @@ impl DocumentUserService for FakeUser { Ok(1) } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { Ok(self.workspace_id.clone()) } @@ -115,8 +115,8 @@ pub fn setup_log() { pub async fn create_and_open_empty_document() -> (DocumentTest, Arc>, String) { let test = DocumentTest::new(); - let doc_id: String = gen_document_id(); - let data = default_document_data(&doc_id); + let doc_id = gen_document_id(); + let data = default_document_data(&doc_id.to_string()); let uid = test.user_service.user_id().unwrap(); // create a document test @@ -130,9 +130,8 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc String { - let uuid = uuid::Uuid::new_v4(); - uuid.to_string() +pub fn gen_document_id() -> Uuid { + uuid::Uuid::new_v4() } pub fn gen_id() -> String { @@ -145,8 +144,8 @@ pub struct LocalTestDocumentCloudServiceImpl(); impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_doc_state( &self, - document_id: &str, - _workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let document_id = document_id.to_string(); Err(FlowyError::new( @@ -157,26 +156,26 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_snapshots( &self, - _document_id: &str, - _limit: usize, - _workspace_id: &str, + document_id: &Uuid, + limit: usize, + workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } async fn get_document_data( &self, - _document_id: &str, - _workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { Ok(None) } async fn create_document_collab( &self, - _workspace_id: &str, - _document_id: &str, - _encoded_collab: EncodedCollab, + workspace_id: &Uuid, + document_id: &Uuid, + encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } @@ -257,14 +256,14 @@ impl DocumentSnapshotService for DocumentTestSnapshot { } struct WorkspaceCollabIntegrateImpl { - workspace_id: String, + workspace_id: Uuid, } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { Ok(self.workspace_id.clone()) } - fn device_id(&self) -> Result { + fn device_id(&self) -> Result { Ok("fake_device_id".to_string()) } } diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 7502370a0f..69897435e9 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -34,6 +34,7 @@ collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } client-api = { workspace = true, optional = true } tantivy = { version = "0.22.0", optional = true } +uuid.workspace = true [features] default = ["impl_from_dispatch_error", "impl_from_serde", "impl_from_reqwest", "impl_from_sqlite"] diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 4cef7a8990..96b9d1c3cf 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -256,3 +256,9 @@ impl From for FlowyError { } } } + +impl From for FlowyError { + fn from(value: uuid::Error) -> Self { + FlowyError::internal().with_context(value) + } +} diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 3e7776d0bf..52ed4b7314 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -15,7 +15,7 @@ pub trait FolderCloudService: Send + Sync + 'static { /// Returns error if the cloud service doesn't support multiple workspaces async fn create_workspace(&self, uid: i64, name: &str) -> Result; - async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>; + async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; /// Returns all workspaces of the user. /// Returns vec![] if the cloud service doesn't support multiple workspaces @@ -23,7 +23,7 @@ pub trait FolderCloudService: Send + Sync + 'static { async fn get_folder_data( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: &i64, ) -> Result, FlowyError>; @@ -35,21 +35,21 @@ pub trait FolderCloudService: Send + Sync + 'static { async fn get_folder_doc_state( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, collab_type: CollabType, - object_id: &str, + object_id: &Uuid, ) -> Result, FlowyError>; async fn full_sync_collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, params: FullSyncCollabParams, ) -> Result<(), FlowyError>; async fn batch_create_folder_collab_objects( &self, - workspace_id: &str, + workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError>; @@ -57,64 +57,64 @@ pub trait FolderCloudService: Send + Sync + 'static { async fn publish_view( &self, - workspace_id: &str, + workspace_id: &Uuid, payload: Vec, ) -> Result<(), FlowyError>; async fn unpublish_views( &self, - workspace_id: &str, - view_ids: Vec, + workspace_id: &Uuid, + view_ids: Vec, ) -> Result<(), FlowyError>; - async fn get_publish_info(&self, view_id: &str) -> Result; + async fn get_publish_info(&self, view_id: &Uuid) -> Result; async fn set_publish_name( &self, - workspace_id: &str, - view_id: String, + workspace_id: &Uuid, + view_id: Uuid, new_name: String, ) -> Result<(), FlowyError>; async fn set_publish_namespace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_namespace: String, ) -> Result<(), FlowyError>; async fn list_published_views( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError>; async fn get_default_published_view_info( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result; async fn set_default_published_view( &self, - workspace_id: &str, + workspace_id: &Uuid, view_id: uuid::Uuid, ) -> Result<(), FlowyError>; - async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError>; + async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; - async fn get_publish_namespace(&self, workspace_id: &str) -> Result; + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result; async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError>; } #[derive(Debug)] pub struct FolderCollabParams { - pub object_id: String, + pub object_id: Uuid, pub encoded_collab_v1: Vec, pub collab_type: CollabType, } #[derive(Debug)] pub struct FullSyncCollabParams { - pub object_id: String, + pub object_id: Uuid, pub encoded_collab: EncodedCollab, pub collab_type: CollabType, } diff --git a/frontend/rust-lib/flowy-folder-pub/src/query.rs b/frontend/rust-lib/flowy-folder-pub/src/query.rs index 7b4682885d..74761e44db 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/query.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/query.rs @@ -3,6 +3,7 @@ use collab_entity::CollabType; use collab_folder::ViewLayout; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub struct QueryCollab { pub collab_type: CollabType, @@ -17,14 +18,14 @@ pub trait FolderQueryService: Send + Sync + 'static { /// the provided view layout, given that the parent view is not a space async fn get_surrounding_view_ids_with_view_layout( &self, - parent_view_id: &str, + parent_view_id: &Uuid, view_layout: ViewLayout, - ) -> Vec; + ) -> Vec; - async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option; + async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option; } #[async_trait] pub trait FolderViewEdit: Send + Sync + 'static { - async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()>; + async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-folder/src/entities/import.rs b/frontend/rust-lib/flowy-folder/src/entities/import.rs index 4189dfaa6d..83e8bdf874 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/import.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/import.rs @@ -4,6 +4,8 @@ use crate::share::{ImportData, ImportItem, ImportParams, ImportType}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::FlowyError; use lib_infra::validator_fn::required_not_empty_str; +use std::str::FromStr; +use uuid::Uuid; use validator::Validate; #[derive(Clone, Debug, ProtoBuf_Enum)] @@ -76,6 +78,8 @@ impl TryInto for ImportPayloadPB { .map_err(|_| FlowyError::invalid_view_id())? .0; + let parent_view_id = Uuid::from_str(&parent_view_id)?; + let items = self .items .into_iter() diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index a8f331a91c..5e29702d53 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -1,12 +1,13 @@ use collab_folder::{View, ViewIcon, ViewLayout}; -use std::collections::HashMap; -use std::convert::TryInto; -use std::ops::{Deref, DerefMut}; -use std::sync::Arc; - use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_folder_pub::cloud::gen_view_id; +use std::collections::HashMap; +use std::convert::TryInto; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; +use std::sync::Arc; +use uuid::Uuid; use crate::entities::icon::ViewIconPB; use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail}; @@ -322,10 +323,10 @@ pub struct CreateOrphanViewPayloadPB { #[derive(Debug, Clone)] pub struct CreateViewParams { - pub parent_view_id: String, + pub parent_view_id: Uuid, pub name: String, pub layout: ViewLayoutPB, - pub view_id: String, + pub view_id: Uuid, pub initial_data: ViewData, pub meta: HashMap, // Mark the view as current view after creation. @@ -346,9 +347,13 @@ impl TryInto for CreateViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; - let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0; + let parent_view_id = ViewIdentify::parse(self.parent_view_id) + .and_then(|id| Uuid::from_str(&id.0).map_err(|err| ErrorCode::InvalidParams))?; // if view_id is not provided, generate a new view_id - let view_id = self.view_id.unwrap_or_else(|| gen_view_id().to_string()); + let view_id = self + .view_id + .and_then(|v| Uuid::parse_str(&v).ok()) + .unwrap_or_else(|| gen_view_id()); Ok(CreateViewParams { parent_view_id, @@ -371,13 +376,13 @@ impl TryInto for CreateOrphanViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; - let parent_view_id = ViewIdentify::parse(self.view_id.clone())?.0; + let view_id = Uuid::parse_str(&self.view_id).map_err(|_| ErrorCode::InvalidParams)?; Ok(CreateViewParams { - parent_view_id, + parent_view_id: view_id.clone(), name, layout: self.layout, - view_id: self.view_id, + view_id, initial_data: ViewData::Data(self.initial_data.into()), meta: Default::default(), set_as_current: false, @@ -564,9 +569,9 @@ impl TryInto for MoveViewPayloadPB { #[derive(Debug)] pub struct MoveNestedViewParams { - pub view_id: String, - pub new_parent_id: String, - pub prev_view_id: Option, + pub view_id: Uuid, + pub new_parent_id: Uuid, + pub prev_view_id: Option, pub from_section: Option, pub to_section: Option, } @@ -575,9 +580,20 @@ impl TryInto for MoveNestedViewPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { - let view_id = ViewIdentify::parse(self.view_id)?.0; + let view_id = Uuid::from_str(&ViewIdentify::parse(self.view_id)?.0) + .map_err(|_| ErrorCode::InvalidParams)?; + let new_parent_id = ViewIdentify::parse(self.new_parent_id)?.0; - let prev_view_id = self.prev_view_id; + let new_parent_id = Uuid::from_str(&new_parent_id).map_err(|_| ErrorCode::InvalidParams)?; + + let prev_view_id = match self.prev_view_id { + Some(prev_view_id) => Some( + Uuid::from_str(&ViewIdentify::parse(prev_view_id)?.0) + .map_err(|_| ErrorCode::InvalidParams)?, + ), + None => None, + }; + Ok(MoveNestedViewParams { view_id, new_parent_id, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 30cbd29d1c..c20eb8a7ad 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -1,8 +1,9 @@ -use std::sync::{Arc, Weak}; -use tracing::instrument; - use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use std::str::FromStr; +use std::sync::{Arc, Weak}; +use tracing::instrument; +use uuid::Uuid; use crate::entities::*; use crate::manager::FolderManager; @@ -443,7 +444,12 @@ pub(crate) async fn unpublish_views_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params = data.into_inner(); - folder.unpublish_views(params.view_ids).await?; + let view_ids = params + .view_ids + .into_iter() + .flat_map(|id| Uuid::from_str(&id).ok()) + .collect::>(); + folder.unpublish_views(view_ids).await?; Ok(()) } @@ -454,6 +460,7 @@ pub(crate) async fn get_publish_info_handler( ) -> DataResult { let folder = upgrade_folder(folder)?; let view_id = data.into_inner().value; + let view_id = Uuid::from_str(&view_id)?; let info = folder.get_publish_info(&view_id).await?; data_result_ok(PublishInfoResponsePB::from(info)) } @@ -465,6 +472,7 @@ pub(crate) async fn set_publish_name_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let SetPublishNamePB { view_id, new_name } = data.into_inner(); + let view_id = Uuid::from_str(&view_id)?; folder.set_publish_name(view_id, new_name).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index e2ec405884..51edf3a11c 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -44,16 +44,18 @@ use flowy_sqlite::kv::KVStorePreferences; use futures::future; use std::collections::HashMap; use std::fmt::{Display, Formatter}; +use std::str::FromStr; use std::sync::{Arc, Weak}; use tokio::sync::RwLockWriteGuard; use tracing::{error, info, instrument}; +use uuid::Uuid; pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult; + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult; } pub struct FolderManager { @@ -111,7 +113,7 @@ impl FolderManager { Ok::(workspace) }; - match folder.get_workspace_info(&workspace_id) { + match folder.get_workspace_info(&workspace_id.to_string()) { None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")), Some(workspace) => workspace_pb_from_workspace(workspace, &folder), } @@ -127,14 +129,14 @@ impl FolderManager { .ok_or_else(|| internal_error("The folder is not initialized"))? .read() .await - .get_folder_data(&workspace_id) + .get_folder_data(&workspace_id.to_string()) .ok_or_else(|| internal_error("Workspace id not match the id in current folder"))?; Ok(data) } pub async fn gather_publish_encode_collab( &self, - view_id: &str, + view_id: &Uuid, layout: &ViewLayout, ) -> FlowyResult { let handler = self.get_handler(layout)?; @@ -177,7 +179,7 @@ impl FolderManager { pub(crate) async fn make_folder>>( &self, uid: i64, - workspace_id: &str, + workspace_id: &Uuid, collab_db: Weak, data_source: Option, folder_notifier: T, @@ -187,8 +189,7 @@ impl FolderManager { let config = CollabBuilderConfig::default().sync_enable(true); let data_source = data_source.unwrap_or_else(|| { - CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string()) - .into_data_source() + CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.clone()).into_data_source() }); let object_id = workspace_id; @@ -218,8 +219,11 @@ impl FolderManager { "Clear the folder data and try to open the folder again due to: {}", err ); + if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { - let _ = db.delete_doc(uid, workspace_id, workspace_id).await; + let _ = db + .delete_doc(uid, &workspace_id.to_string(), &object_id.to_string()) + .await; } Err(err.into()) }, @@ -229,7 +233,7 @@ impl FolderManager { pub(crate) async fn create_folder_with_data( &self, uid: i64, - workspace_id: &str, + workspace_id: &Uuid, collab_db: Weak, notifier: Option, folder_data: Option, @@ -240,8 +244,8 @@ impl FolderManager { .collab_builder .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; - let doc_state = CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string()) - .into_data_source(); + let doc_state = + CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.clone()).into_data_source(); let folder = self .collab_builder .create_folder( @@ -317,7 +321,7 @@ impl FolderManager { _token: &str, is_new: bool, data_source: FolderInitDataSource, - workspace_id: &str, + workspace_id: &Uuid, ) -> FlowyResult<()> { // Create the default workspace if the user is new info!("initialize_when_sign_up: is_new: {}", is_new); @@ -377,7 +381,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; Ok(WorkspaceSettingPB { - workspace_id, + workspace_id: workspace_id.to_string(), latest_view, }) } @@ -495,7 +499,7 @@ impl FolderManager { .ok_or_else(|| FlowyError::internal().with_context("folder is not initialized"))?; let folder = lock.read().await; let workspace = folder - .get_workspace_info(&workspace_id) + .get_workspace_info(&workspace_id.to_string()) .ok_or_else(|| FlowyError::record_not_found().with_context("Can not find the workspace"))?; let views = folder @@ -606,8 +610,9 @@ impl FolderManager { // Drop the folder lock explicitly to avoid deadlock when following calls contains 'self' drop(folder); + let view_id = Uuid::from_str(view_id)?; let handler = self.get_handler(&view.layout)?; - handler.close_view(view_id).await?; + handler.close_view(&view_id).await?; } } Ok(()) @@ -844,24 +849,28 @@ impl FolderManager { let prev_view_id = params.prev_view_id; let from_section = params.from_section; let to_section = params.to_section; - let view = self.get_view_pb(&view_id).await?; + let view = self.get_view_pb(&view_id.to_string()).await?; // if the view is locked, the view can't be moved if view.is_locked.unwrap_or(false) { return Err(FlowyError::view_is_locked()); } - let old_parent_id = view.parent_view_id; + let old_parent_id = Uuid::from_str(&view.parent_view_id)?; if let Some(lock) = self.mutex_folder.load_full() { let mut folder = lock.write().await; - folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); + folder.move_nested_view( + &view_id.to_string(), + &new_parent_id.to_string(), + prev_view_id.map(|s| s.to_string()), + ); if from_section != to_section { if to_section == Some(ViewSectionPB::Private) { - folder.add_private_view_ids(vec![view_id.clone()]); + folder.add_private_view_ids(vec![view_id.to_string()]); } else { - folder.delete_private_view_ids(vec![view_id.clone()]); + folder.delete_private_view_ids(vec![view_id.to_string()]); } } - notify_parent_view_did_change(&workspace_id, &folder, vec![new_parent_id, old_parent_id]); + notify_parent_view_did_change(workspace_id, &folder, vec![new_parent_id, old_parent_id]); } Ok(()) } @@ -912,7 +921,8 @@ impl FolderManager { if let Some(lock) = self.mutex_folder.load_full() { let mut folder = lock.write().await; folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); - notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]); + let parent_view_id = Uuid::from_str(&parent_view_id)?; + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); } } } @@ -1115,7 +1125,8 @@ impl FolderManager { view.name, view.layout ); - let view_data = handler.duplicate_view(&view.id).await?; + let view_id = Uuid::from_str(&view.id)?; + let view_data = handler.duplicate_view(&view_id).await?; let index = self .get_view_relation(¤t_parent_id) @@ -1151,12 +1162,13 @@ impl FolderManager { view.name.clone() }; + let parent_view_id = Uuid::from_str(¤t_parent_id)?; let duplicate_params = CreateViewParams { - parent_view_id: current_parent_id.clone(), + parent_view_id, name, layout: view.layout.clone().into(), initial_data: ViewData::DuplicateData(view_data), - view_id: gen_view_id().to_string(), + view_id: gen_view_id(), meta: Default::default(), set_as_current: is_source_view && open_after_duplicated, index, @@ -1176,7 +1188,7 @@ impl FolderManager { if sync_after_create { if let Some(encoded_collab) = encoded_collab { - let object_id = duplicated_view.id.clone(); + let object_id = Uuid::from_str(&duplicated_view.id)?; let collab_type = match duplicated_view.layout { ViewLayout::Document => CollabType::Document, ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database, @@ -1208,20 +1220,20 @@ impl FolderManager { is_source_view = false } - let workspace_id = &self.user.workspace_id()?; + let workspace_id = self.user.workspace_id()?; + let parent_view_id = Uuid::from_str(&parent_view_id)?; // Sync the view to the cloud if sync_after_create { self .cloud_service - .batch_create_folder_collab_objects(workspace_id, objects) + .batch_create_folder_collab_objects(&workspace_id, objects) .await?; } // notify the update here let folder = lock.read().await; - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id.to_string()]); - + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); let duplicated_view = self.get_view_pb(&new_view_id).await?; Ok(duplicated_view) @@ -1242,6 +1254,7 @@ impl FolderManager { let view_layout: ViewLayout = view.layout.clone().into(); if let Some(handle) = self.operation_handlers.get(&view_layout) { info!("Open view: {}-{}", view.name, view.id); + let view_id = Uuid::from_str(&view.id)?; if let Err(err) = handle.open_view(&view_id).await { error!("Open view error: {:?}", err); } @@ -1250,7 +1263,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; let setting = WorkspaceSettingPB { - workspace_id, + workspace_id: workspace_id.to_string(), latest_view: view, }; send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting); @@ -1367,18 +1380,18 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; self .cloud_service - .publish_view(workspace_id.as_str(), payload) + .publish_view(&workspace_id, payload) .await?; Ok(()) } /// Unpublish the view with the given view id. #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn unpublish_views(&self, view_ids: Vec) -> FlowyResult<()> { + pub async fn unpublish_views(&self, view_ids: Vec) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; self .cloud_service - .unpublish_views(workspace_id.as_str(), view_ids) + .unpublish_views(&workspace_id, view_ids) .await?; Ok(()) } @@ -1386,14 +1399,14 @@ impl FolderManager { /// Get the publish info of the view with the given view id. /// The publish info contains the namespace and publish_name of the view. #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult { + pub async fn get_publish_info(&self, view_id: &Uuid) -> FlowyResult { let publish_info = self.cloud_service.get_publish_info(view_id).await?; Ok(publish_info) } /// Sets the publish name of the view with the given view id. #[tracing::instrument(level = "debug", skip(self))] - pub async fn set_publish_name(&self, view_id: String, new_name: String) -> FlowyResult<()> { + pub async fn set_publish_name(&self, view_id: Uuid, new_name: String) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; self .cloud_service @@ -1409,7 +1422,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; self .cloud_service - .set_publish_namespace(workspace_id.as_str(), new_namespace) + .set_publish_namespace(&workspace_id, new_namespace) .await?; Ok(()) } @@ -1420,7 +1433,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; let namespace = self .cloud_service - .get_publish_namespace(workspace_id.as_str()) + .get_publish_namespace(&workspace_id) .await?; Ok(namespace) } @@ -1502,7 +1515,7 @@ impl FolderManager { }; if let Ok(payload) = self - .get_publish_payload(¤t_view_id, publish_name, layout) + .get_publish_payload(&Uuid::from_str(¤t_view_id)?, publish_name, layout) .await { payloads.push(payload); @@ -1551,7 +1564,7 @@ impl FolderManager { async fn get_publish_payload( &self, - view_id: &str, + view_id: &Uuid, publish_name: Option, layout: ViewLayout, ) -> FlowyResult { @@ -1559,18 +1572,20 @@ impl FolderManager { let encoded_collab_wrapper: GatherEncodedCollab = handler .gather_publish_encode_collab(&self.user, view_id) .await?; - let view = self.get_view_pb(view_id).await?; + + let view_str_id = view_id.to_string(); + let view = self.get_view_pb(&view_str_id).await?; let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); let child_views = self - .build_publish_views(view_id) + .build_publish_views(&view_str_id) .await .and_then(|v| v.child_views) .unwrap_or_default(); let ancestor_views = self - .get_view_ancestors_pb(view_id) + .get_view_ancestors_pb(&view_str_id) .await? .iter() .map(view_pb_to_publish_view) @@ -1720,8 +1735,9 @@ impl FolderManager { }; if let Some(view) = view { + let view_id = Uuid::from_str(view_id)?; if let Ok(handler) = self.get_handler(&view.layout) { - handler.delete_view(view_id).await?; + handler.delete_view(&view_id).await?; } } } @@ -1733,11 +1749,11 @@ impl FolderManager { #[instrument(level = "debug", skip_all, err)] pub(crate) async fn import_single_file( &self, - parent_view_id: String, + parent_view_id: Uuid, import_data: ImportItem, ) -> FlowyResult<(View, Vec<(String, CollabType, EncodedCollab)>)> { let handler = self.get_handler(&import_data.view_layout)?; - let view_id = gen_view_id().to_string(); + let view_id = gen_view_id(); let uid = self.user.user_id()?; let mut encoded_collab = vec![]; @@ -1745,7 +1761,7 @@ impl FolderManager { match import_data.data { ImportData::FilePath { file_path } => { handler - .import_from_file_path(&view_id, &import_data.name, file_path) + .import_from_file_path(&view_id.to_string(), &import_data.name, file_path) .await?; }, ImportData::Bytes { bytes } => { @@ -1804,11 +1820,13 @@ impl FolderManager { views.push(view_pb_without_child_views(view)); for (object_id, collab_type, encode_collab) in encoded_collabs { - match self.get_folder_collab_params(object_id, collab_type, encode_collab) { - Ok(params) => objects.push(params), - Err(e) => { - error!("import error {}", e); - }, + if let Ok(object_id) = Uuid::from_str(&object_id) { + match self.get_folder_collab_params(object_id, collab_type, encode_collab) { + Ok(params) => objects.push(params), + Err(e) => { + error!("import error {}", e); + }, + } } } } @@ -1822,7 +1840,7 @@ impl FolderManager { // Notify that the parent view has changed if let Some(lock) = self.mutex_folder.load_full() { let folder = lock.read().await; - notify_parent_view_did_change(&workspace_id, &folder, vec![import_data.parent_view_id]); + notify_parent_view_did_change(workspace_id, &folder, vec![import_data.parent_view_id]); } Ok(RepeatedViewPB { items: views }) @@ -1887,7 +1905,7 @@ impl FolderManager { fn get_folder_collab_params( &self, - object_id: String, + object_id: Uuid, collab_type: CollabType, encoded_collab: EncodedCollab, ) -> FlowyResult { @@ -1911,18 +1929,20 @@ impl FolderManager { let folder = lock.read().await; let view = folder.get_view(view_id)?; match folder.get_view(&view.parent_view_id) { - None => folder.get_workspace_info(&workspace_id).map(|workspace| { - ( - true, - workspace.id, - workspace - .child_views - .items - .into_iter() - .map(|view| view.id) - .collect::>(), - ) - }), + None => folder + .get_workspace_info(&workspace_id.to_string()) + .map(|workspace| { + ( + true, + workspace.id, + workspace + .child_views + .items + .into_iter() + .map(|view| view.id) + .collect::>(), + ) + }), Some(parent_view) => Some(( false, parent_view.id.clone(), @@ -2031,17 +2051,17 @@ impl FolderManager { .collect() } - pub fn remove_indices_for_workspace(&self, workspace_id: String) -> FlowyResult<()> { + pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { self .folder_indexer - .remove_indices_for_workspace(workspace_id)?; + .remove_indices_for_workspace(workspace_id.clone())?; Ok(()) } } /// Return the views that belong to the workspace. The views are filtered by the trash and all the private views. -pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) -> Vec { +pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2056,7 +2076,7 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) .map(|view| view.id) .collect::>(); - let mut views = folder.get_views_belong_to(workspace_id); + let mut views = folder.get_views_belong_to(&workspace_id.to_string()); // filter the views that are in the trash and all the private views views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); @@ -2082,7 +2102,7 @@ fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec { } /// Get the current private views of the user. -pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder) -> Vec { +pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2097,7 +2117,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder .map(|view| view.id) .collect::>(); - let mut views = folder.get_views_belong_to(workspace_id); + let mut views = folder.get_views_belong_to(&workspace_id.to_string()); // filter the views that are in the trash and not in the private view ids views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index 4393bfbb29..c319d2ef77 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -10,6 +10,7 @@ use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; use tokio::task::spawn_blocking; use tracing::{event, info, Level}; +use uuid::Uuid; impl FolderManager { /// Called immediately after the application launched if the user already sign in/sign up. @@ -17,7 +18,7 @@ impl FolderManager { pub async fn initialize( &self, uid: i64, - workspace_id: &str, + workspace_id: &Uuid, initial_data: FolderInitDataSource, ) -> FlowyResult<()> { // Update the workspace id @@ -37,7 +38,6 @@ impl FolderManager { ); } - let workspace_id = workspace_id.to_string(); // Get the collab db for the user with given user id. let collab_db = self.user.collab_db(uid)?; @@ -54,20 +54,20 @@ impl FolderManager { } => { let is_exist = self .user - .is_folder_exist_on_disk(uid, &workspace_id) + .is_folder_exist_on_disk(uid, workspace_id) .unwrap_or(false); // 1. if the folder exists, open it from local disk if is_exist { event!(Level::INFO, "Init folder from local disk"); self - .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) + .make_folder(uid, workspace_id, collab_db, None, folder_notifier) .await? } else if create_if_not_exist { // 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder // Currently, this branch is only used when the server type is supabase. For appflowy cloud, // the default workspace is already created when the user sign up. self - .create_default_folder(uid, &workspace_id, collab_db, folder_notifier) + .create_default_folder(uid, workspace_id, collab_db, folder_notifier) .await? } else { // 3. If the folder doesn't exist and create_if_not_exist is false, try to fetch the folder data from cloud/ @@ -147,7 +147,7 @@ impl FolderManager { async fn create_default_folder( &self, uid: i64, - workspace_id: &str, + workspace_id: &Uuid, collab_db: Weak, folder_notifier: FolderNotify, ) -> Result>, FlowyError> { @@ -170,24 +170,22 @@ impl FolderManager { Ok(folder) } - fn handle_index_folder(&self, workspace_id: String, folder: &Folder) { + fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) { let mut index_all = true; let encoded_collab = self .store_preferences - .get_object::(&workspace_id); + .get_object::(workspace_id.to_string().as_str()); if let Some(encoded_collab) = encoded_collab { if let Ok(changes) = folder.calculate_view_changes(encoded_collab) { let folder_indexer = self.folder_indexer.clone(); let views = folder.get_all_views(); - let wid = workspace_id.clone(); - if !changes.is_empty() && !views.is_empty() { spawn_blocking(move || { // We index the changes - folder_indexer.index_view_changes(views, changes, wid); + folder_indexer.index_view_changes(views, changes, workspace_id); }); index_all = false; } @@ -197,15 +195,13 @@ impl FolderManager { if index_all { let views = folder.get_all_views(); let folder_indexer = self.folder_indexer.clone(); - let wid = workspace_id.clone(); - // We spawn a blocking task to index all views in the folder spawn_blocking(move || { // We remove old indexes just in case - let _ = folder_indexer.remove_indices_for_workspace(wid.clone()); + let _ = folder_indexer.remove_indices_for_workspace(workspace_id.clone()); // We index all views from the workspace - folder_indexer.index_all_views(views, wid); + folder_indexer.index_all_views(views, workspace_id); }); } diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index dec4ff062d..a6faede8b5 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -13,14 +13,16 @@ use collab_folder::{ use lib_infra::sync_trace; use std::collections::HashSet; +use std::str::FromStr; use std::sync::Weak; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; use tracing::{event, trace, Level}; +use uuid::Uuid; /// Listen on the [ViewChange] after create/delete/update events happened pub(crate) fn subscribe_folder_view_changed( - workspace_id: String, + workspace_id: Uuid, mut rx: ViewChangeReceiver, weak_mutex_folder: Weak>, user: Weak, @@ -46,9 +48,10 @@ pub(crate) fn subscribe_folder_view_changed( ChildViewChangeReason::Create, ); let folder = lock.read().await; - let parent_view_id = view.parent_view_id.clone(); - notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]); - sync_trace!("[Folder] create view: {:?}", view); + if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); + sync_trace!("[Folder] create view: {:?}", view); + } }, ViewChange::DidDeleteView { views } => { for view in views { @@ -69,7 +72,9 @@ pub(crate) fn subscribe_folder_view_changed( ChildViewChangeReason::Update, ); let folder = lock.read().await; - notify_parent_view_did_change(&workspace_id, &folder, vec![view.parent_view_id]); + if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); + } }, }; } @@ -78,7 +83,7 @@ pub(crate) fn subscribe_folder_view_changed( } pub(crate) fn subscribe_folder_sync_state_changed( - workspace_id: String, + workspace_id: Uuid, mut folder_sync_state_rx: WatchStream, user: Weak, ) { @@ -93,16 +98,19 @@ pub(crate) fn subscribe_folder_sync_state_changed( } } - folder_notification_builder(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) - .payload(FolderSyncStatePB::from(state)) - .send(); + folder_notification_builder( + &workspace_id.to_string(), + FolderNotification::DidUpdateFolderSyncUpdate, + ) + .payload(FolderSyncStatePB::from(state)) + .send(); } }); } /// Listen on the [TrashChange]s and notify the frontend some views were changed. pub(crate) fn subscribe_folder_trash_changed( - workspace_id: String, + workspace_id: Uuid, mut rx: SectionChangeReceiver, weak_mutex_folder: Weak>, user: Weak, @@ -131,7 +139,9 @@ pub(crate) fn subscribe_folder_trash_changed( let folder = lock.read().await; let views = folder.get_views(&ids); for view in views { - unique_ids.insert(view.parent_view_id.clone()); + if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { + unique_ids.insert(parent_view_id); + } } let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); @@ -140,7 +150,7 @@ pub(crate) fn subscribe_folder_trash_changed( .send(); let parent_view_ids = unique_ids.into_iter().collect(); - notify_parent_view_did_change(&workspace_id, &folder, parent_view_ids); + notify_parent_view_did_change(workspace_id, &folder, parent_view_ids); }, } } @@ -150,10 +160,10 @@ pub(crate) fn subscribe_folder_trash_changed( /// Notify the list of parent view ids that its child views were changed. #[tracing::instrument(level = "debug", skip(folder, parent_view_ids))] -pub(crate) fn notify_parent_view_did_change>( - workspace_id: &str, +pub(crate) fn notify_parent_view_did_change( + workspace_id: Uuid, folder: &Folder, - parent_view_ids: Vec, + parent_view_ids: Vec, ) -> Option<()> { let trash_ids = folder .get_all_trash_sections() @@ -162,24 +172,23 @@ pub(crate) fn notify_parent_view_did_change>( .collect::>(); for parent_view_id in parent_view_ids { - let parent_view_id = parent_view_id.as_ref(); - // if the view's parent id equal to workspace id. Then it will fetch the current // workspace views. Because the workspace is not a view stored in the views map. if parent_view_id == workspace_id { - notify_did_update_workspace(workspace_id, folder); - notify_did_update_section_views(workspace_id, folder); + notify_did_update_workspace(&workspace_id, folder); + notify_did_update_section_views(&workspace_id, folder); } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. - let parent_view = folder.get_view(parent_view_id)?; - let mut child_views = folder.get_views_belong_to(parent_view_id); + let parent_view_id = parent_view_id.to_string(); + let parent_view = folder.get_view(&parent_view_id)?; + let mut child_views = folder.get_views_belong_to(&parent_view_id); child_views.retain(|view| !trash_ids.contains(&view.id)); event!(Level::DEBUG, child_views_count = child_views.len()); // Post the notification let parent_view_pb = view_pb_with_child_views(parent_view, child_views); - folder_notification_builder(parent_view_id, FolderNotification::DidUpdateView) + folder_notification_builder(&parent_view_id, FolderNotification::DidUpdateView) .payload(parent_view_pb) .send(); } @@ -188,7 +197,7 @@ pub(crate) fn notify_parent_view_did_change>( None } -pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) { +pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Folder) { let public_views = get_workspace_public_view_pbs(workspace_id, folder); let private_views = get_workspace_private_view_pbs(workspace_id, folder); trace!( @@ -214,7 +223,7 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folde .send(); } -pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) { +pub(crate) fn notify_did_update_workspace(workspace_id: &Uuid, folder: &Folder) { let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into(); folder_notification_builder(workspace_id, FolderNotification::DidUpdateWorkspaceViews) .payload(repeated_view) diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index 8d9bfd5dea..5629ef4133 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -1,6 +1,7 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; use lib_dispatch::prelude::ToBytes; +use tracing::trace; const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; @@ -68,9 +69,14 @@ impl std::convert::From for FolderNotification { } } -#[tracing::instrument(level = "trace")] -pub(crate) fn folder_notification_builder(id: &str, ty: FolderNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, FOLDER_OBSERVABLE_SOURCE) +#[tracing::instrument(level = "trace", skip_all)] +pub(crate) fn folder_notification_builder( + id: T, + ty: FolderNotification, +) -> NotificationBuilder { + let id = id.to_string(); + trace!("folder_notification_builder: id = {id}, ty = {ty:?}"); + NotificationBuilder::new(&id, ty, FOLDER_OBSERVABLE_SOURCE) } /// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the diff --git a/frontend/rust-lib/flowy-folder/src/share/import.rs b/frontend/rust-lib/flowy-folder/src/share/import.rs index 2abac3540d..6fd8d8feab 100644 --- a/frontend/rust-lib/flowy-folder/src/share/import.rs +++ b/frontend/rust-lib/flowy-folder/src/share/import.rs @@ -1,5 +1,6 @@ use collab_folder::ViewLayout; use std::fmt::{Display, Formatter}; +use uuid::Uuid; #[derive(Clone, Debug)] pub enum ImportType { @@ -35,6 +36,6 @@ impl Display for ImportData { #[derive(Clone, Debug)] pub struct ImportParams { - pub parent_view_id: String, + pub parent_view_id: Uuid, pub items: Vec, } diff --git a/frontend/rust-lib/flowy-folder/src/util.rs b/frontend/rust-lib/flowy-folder/src/util.rs index 89d49f8a23..98b87be52d 100644 --- a/frontend/rust-lib/flowy-folder/src/util.rs +++ b/frontend/rust-lib/flowy-folder/src/util.rs @@ -1,11 +1,12 @@ use crate::entities::UserFolderPB; use flowy_error::{ErrorCode, FlowyError}; +use uuid::Uuid; pub(crate) fn folder_not_init_error() -> FlowyError { FlowyError::internal().with_context("Folder not initialized") } -pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError { +pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &Uuid) -> FlowyError { FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB { uid, workspace_id: workspace_id.to_string(), diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 2b0a9667c9..17919e07b1 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -6,11 +6,11 @@ use collab_folder::hierarchy_builder::NestedViewBuilder; pub use collab_folder::View; use collab_folder::ViewLayout; use dashmap::DashMap; +use flowy_error::FlowyError; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; - -use flowy_error::FlowyError; +use uuid::Uuid; use lib_infra::util::timestamp; @@ -51,23 +51,23 @@ pub trait FolderOperationHandler: Send + Sync { Ok(()) } - async fn open_view(&self, view_id: &str) -> Result<(), FlowyError>; + async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; /// Closes the view and releases the resources that this view has in /// the backend - async fn close_view(&self, view_id: &str) -> Result<(), FlowyError>; + async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; /// Called when the view is deleted. /// This will called after the view is deleted from the trash. - async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError>; + async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; /// Returns the [ViewData] that can be used to create the same view. - async fn duplicate_view(&self, view_id: &str) -> Result; + async fn duplicate_view(&self, view_id: &Uuid) -> Result; /// get the encoded collab data from the disk. async fn gather_publish_encode_collab( &self, _user: &Arc, - _view_id: &str, + _view_id: &Uuid, ) -> Result { Err(FlowyError::not_support()) } @@ -102,8 +102,8 @@ pub trait FolderOperationHandler: Send + Sync { async fn create_default_view( &self, user_id: i64, - parent_view_id: &str, - view_id: &str, + parent_view_id: &Uuid, + view_id: &Uuid, name: &str, layout: ViewLayout, ) -> Result<(), FlowyError>; @@ -114,7 +114,7 @@ pub trait FolderOperationHandler: Send + Sync { async fn import_from_bytes( &self, uid: i64, - view_id: &str, + view_id: &Uuid, name: &str, import_type: ImportType, bytes: Vec, @@ -152,8 +152,8 @@ impl From for ViewLayout { pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout) -> View { let time = timestamp(); View { - id: params.view_id, - parent_view_id: params.parent_view_id, + id: params.view_id.to_string(), + parent_view_id: params.parent_view_id.to_string(), name: params.name, created_at: time, is_favorite: false, diff --git a/frontend/rust-lib/flowy-search-pub/Cargo.toml b/frontend/rust-lib/flowy-search-pub/Cargo.toml index 631f2d2c83..907942303d 100644 --- a/frontend/rust-lib/flowy-search-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-search-pub/Cargo.toml @@ -11,4 +11,4 @@ collab = { workspace = true } collab-folder = { workspace = true } flowy-error = { workspace = true } client-api = { workspace = true } -futures = { workspace = true } +uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-search-pub/src/cloud.rs b/frontend/rust-lib/flowy-search-pub/src/cloud.rs index f2ffb3c439..b62e226b92 100644 --- a/frontend/rust-lib/flowy-search-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-search-pub/src/cloud.rs @@ -1,12 +1,13 @@ use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use uuid::Uuid; #[async_trait] pub trait SearchCloudService: Send + Sync + 'static { async fn document_search( &self, - workspace_id: &str, + workspace_id: &Uuid, query: String, ) -> Result, FlowyError>; } diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs index 65e23a9ddb..b7145e993f 100644 --- a/frontend/rust-lib/flowy-search-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -4,44 +4,45 @@ use std::sync::Arc; use collab::core::collab::IndexContentReceiver; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout}; use flowy_error::FlowyError; +use uuid::Uuid; pub struct IndexableData { pub id: String, pub data: String, pub icon: Option, pub layout: ViewLayout, - pub workspace_id: String, + pub workspace_id: Uuid, } impl IndexableData { - pub fn from_view(view: Arc, workspace_id: String) -> Self { + pub fn from_view(view: Arc, workspace_id: Uuid) -> Self { IndexableData { id: view.id.clone(), data: view.name.clone(), icon: view.icon.clone(), layout: view.layout.clone(), - workspace_id: workspace_id.clone(), + workspace_id, } } } pub trait IndexManager: Send + Sync { - fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: String); + fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid); fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; - fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError>; + fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>; fn is_indexed(&self) -> bool; fn as_any(&self) -> &dyn Any; } pub trait FolderIndexManager: IndexManager { - fn index_all_views(&self, views: Vec>, workspace_id: String); + fn index_all_views(&self, views: Vec>, workspace_id: Uuid); fn index_view_changes( &self, views: Vec>, changes: Vec, - workspace_id: String, + workspace_id: Uuid, ); } diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index f4d6207783..dd313d1aea 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -18,7 +18,6 @@ flowy-error = { workspace = true, features = [ "impl_from_serde", ] } flowy-notification.workspace = true -flowy-sqlite.workspace = true flowy-user.workspace = true flowy-search-pub.workspace = true flowy-folder = { workspace = true } @@ -37,12 +36,7 @@ async-stream = "0.3.4" strsim = "0.11.0" strum_macros = "0.26.1" tantivy = { version = "0.22.0" } -tempfile = "3.9.0" -validator = { workspace = true, features = ["derive"] } - -diesel.workspace = true -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } -diesel_migrations = { version = "2.1.0", features = ["sqlite"] } +uuid.workspace = true [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index 4f963033e0..d447d27dfb 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,10 +1,11 @@ -use std::sync::Arc; -use tracing::{trace, warn}; - use flowy_error::FlowyResult; use flowy_folder::{manager::FolderManager, ViewLayout}; use flowy_search_pub::cloud::SearchCloudService; use lib_infra::async_trait::async_trait; +use std::str::FromStr; +use std::sync::Arc; +use tracing::{trace, warn}; +use uuid::Uuid; use crate::{ entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB}, @@ -49,6 +50,7 @@ impl SearchHandler for DocumentSearchHandler { None => return Ok(vec![]), }; + let workspace_id = Uuid::from_str(&workspace_id)?; let results = self .cloud_service .document_search(&workspace_id, query) @@ -61,7 +63,7 @@ impl SearchHandler for DocumentSearchHandler { let mut search_results: Vec = vec![]; for result in results { - if let Some(view) = views.iter().find(|v| v.id == result.object_id) { + if let Some(view) = views.iter().find(|v| v.id == result.object_id.to_string()) { // If there is no View for the result, we don't add it to the results // If possible we will extract the icon to display for the result let icon: Option = match view.icon.clone() { @@ -77,12 +79,12 @@ impl SearchHandler for DocumentSearchHandler { search_results.push(SearchResultPB { index_type: IndexTypePB::Document, - view_id: result.object_id.clone(), - id: result.object_id.clone(), + view_id: result.object_id.to_string(), + id: result.object_id.to_string(), data: view.name.clone(), icon, score: result.score, - workspace_id: result.workspace_id, + workspace_id: result.workspace_id.to_string(), preview: result.preview, }); } else { diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 8c1d5633ac..509abcb955 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -20,13 +20,13 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; use flowy_user::services::authenticate_user::AuthenticateUser; +use super::entities::FolderIndexData; use strsim::levenshtein; use tantivy::{ collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, Index, IndexReader, IndexWriter, TantivyDocument, Term, }; - -use super::entities::FolderIndexData; +use uuid::Uuid; #[derive(Clone)] pub struct FolderIndexManagerImpl { @@ -139,7 +139,7 @@ impl FolderIndexManagerImpl { title_field => data.data.clone(), icon_field => icon.unwrap_or_default(), icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.clone(), + workspace_id_field => data.workspace_id.to_string(), ]); } @@ -293,7 +293,7 @@ impl IndexManager for FolderIndexManagerImpl { .unwrap_or(false) } - fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: String) { + fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { let indexer = self.clone(); let wid = workspace_id.clone(); tokio::spawn(async move { @@ -352,7 +352,7 @@ impl IndexManager for FolderIndexManagerImpl { title_field => data.data, icon_field => icon.unwrap_or_default(), icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.clone(), + workspace_id_field => data.workspace_id.to_string(), ]); index_writer.commit()?; @@ -389,7 +389,7 @@ impl IndexManager for FolderIndexManagerImpl { title_field => data.data, icon_field => icon.unwrap_or_default(), icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id, + workspace_id_field => data.workspace_id.to_string(), ]); index_writer.commit()?; @@ -400,14 +400,14 @@ impl IndexManager for FolderIndexManagerImpl { /// Removes all indexes that are related by workspace id. This is useful /// for cleaning indexes when eg. removing/leaving a workspace. /// - fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError> { + fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> { let mut index_writer = self.get_index_writer()?; let folder_schema = self.get_folder_schema()?; let id_field = folder_schema .schema .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - let delete_term = Term::from_field_text(id_field, &workspace_id); + let delete_term = Term::from_field_text(id_field, &workspace_id.to_string()); index_writer.delete_term(delete_term); index_writer.commit()?; @@ -421,7 +421,7 @@ impl IndexManager for FolderIndexManagerImpl { } impl FolderIndexManager for FolderIndexManagerImpl { - fn index_all_views(&self, views: Vec>, workspace_id: String) { + fn index_all_views(&self, views: Vec>, workspace_id: Uuid) { let indexable_data = views .into_iter() .map(|view| IndexableData::from_view(view, workspace_id.clone())) @@ -434,7 +434,7 @@ impl FolderIndexManager for FolderIndexManagerImpl { &self, views: Vec>, changes: Vec, - workspace_id: String, + workspace_id: Uuid, ) { let mut views_iter = views.into_iter(); for change in changes { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index b0f09b1530..db75ea93fe 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,4 +1,5 @@ use flowy_error::FlowyResult; +use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; pub const USER_UUID: &str = "uuid"; @@ -8,5 +9,5 @@ pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. pub trait ServerUser: Send + Sync { /// different user might return different workspace id. - fn workspace_id(&self) -> FlowyResult; + fn workspace_id(&self) -> FlowyResult; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index e14f247d88..6f5edc93cc 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -18,6 +18,7 @@ use serde_json::Value; use std::collections::HashMap; use std::path::Path; use tracing::trace; +use uuid::Uuid; pub(crate) struct AFCloudChatCloudServiceImpl { pub inner: T, @@ -30,10 +31,10 @@ where { async fn create_chat( &self, - _uid: &i64, - workspace_id: &str, - chat_id: &str, - rag_ids: Vec, + uid: &i64, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, ) -> Result<(), FlowyError> { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); @@ -52,13 +53,12 @@ where async fn create_question( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, metadata: &[ChatMessageMetadata], ) -> Result { - let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatMessageParams { @@ -68,7 +68,7 @@ where }; let message = try_get_client? - .create_question(&workspace_id, &chat_id, params) + .create_question(workspace_id, &chat_id, params) .await .map_err(FlowyError::from)?; Ok(message) @@ -76,8 +76,8 @@ where async fn create_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -89,7 +89,7 @@ where question_message_id: question_id, }; let message = try_get_client? - .save_answer(workspace_id, chat_id, params) + .save_answer(workspace_id, chat_id.to_string().as_str(), params) .await .map_err(FlowyError::from)?; Ok(message) @@ -97,8 +97,8 @@ where async fn stream_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, format: ResponseFormat, ai_model: Option, @@ -129,13 +129,17 @@ where async fn get_answer( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, question_message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_answer(workspace_id, chat_id, question_message_id) + .get_answer( + workspace_id, + chat_id.to_string().as_str(), + question_message_id, + ) .await .map_err(FlowyError::from)?; Ok(resp) @@ -143,14 +147,14 @@ where async fn get_chat_messages( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, offset: MessageCursor, limit: u64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_chat_messages(workspace_id, chat_id, offset, limit) + .get_chat_messages(workspace_id, chat_id.to_string().as_str(), offset, limit) .await .map_err(FlowyError::from)?; @@ -159,13 +163,17 @@ where async fn get_question_from_answer_id( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, answer_message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client()?; let resp = try_get_client - .get_question_message_from_answer_id(workspace_id, chat_id, answer_message_id) + .get_question_message_from_answer_id( + workspace_id, + chat_id.to_string().as_str(), + answer_message_id, + ) .await .map_err(FlowyError::from)? .ok_or_else(FlowyError::record_not_found)?; @@ -175,13 +183,13 @@ where async fn get_related_message( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_chat_related_question(workspace_id, chat_id, message_id) + .get_chat_related_question(workspace_id, chat_id.to_string().as_str(), message_id) .await .map_err(FlowyError::from)?; @@ -190,7 +198,7 @@ where async fn stream_complete( &self, - workspace_id: &str, + workspace_id: &Uuid, params: CompleteTextParams, ai_model: Option, ) -> Result { @@ -207,10 +215,10 @@ where async fn embed_file( &self, - _workspace_id: &str, - _file_path: &Path, - _chat_id: &str, - _metadata: Option>, + workspace_id: &Uuid, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, ) -> Result<(), FlowyError> { return Err( FlowyError::not_support() @@ -218,7 +226,7 @@ where ); } - async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { let system = get_operating_system(); let platform = match system { OperatingSystem::MacOS => "macos", @@ -232,51 +240,51 @@ where let config = self .inner .try_get_client()? - .get_local_ai_config(workspace_id, platform) + .get_local_ai_config(workspace_id.to_string().as_str(), platform) .await?; Ok(config) } async fn get_workspace_plan( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { let plans = self .inner .try_get_client()? - .get_active_workspace_subscriptions(workspace_id) + .get_active_workspace_subscriptions(workspace_id.to_string().as_str()) .await?; Ok(plans) } async fn get_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, ) -> Result { let settings = self .inner .try_get_client()? - .get_chat_settings(workspace_id, chat_id) + .get_chat_settings(workspace_id, chat_id.to_string().as_str()) .await?; Ok(settings) } async fn update_chat_settings( &self, - workspace_id: &str, - chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, params: UpdateChatParams, ) -> Result<(), FlowyError> { self .inner .try_get_client()? - .update_chat_settings(workspace_id, chat_id, params) + .update_chat_settings(workspace_id, chat_id.to_string().as_str(), params) .await?; Ok(()) } - async fn get_available_models(&self, workspace_id: &str) -> Result { + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { let list = self .inner .try_get_client()? @@ -285,11 +293,11 @@ where Ok(list) } - async fn get_workspace_default_model(&self, workspace_id: &str) -> Result { + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { let setting = self .inner .try_get_client()? - .get_workspace_settings(workspace_id) + .get_workspace_settings(workspace_id.to_string().as_str()) .await?; Ok(setting.ai_model) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 51e01bf4c1..f862c57fc4 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,3 +1,6 @@ +use crate::af_cloud::define::ServerUser; +use crate::af_cloud::impls::util::check_request_workspace_id_is_match; +use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams, }; @@ -6,20 +9,16 @@ use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; use collab::entity::EncodedCollab; use collab_entity::CollabType; -use serde_json::{Map, Value}; -use std::sync::Arc; -use tracing::{error, instrument}; - use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, TranslateRowContent, TranslateRowResponse, }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; - -use crate::af_cloud::define::ServerUser; -use crate::af_cloud::impls::util::check_request_workspace_id_is_match; -use crate::af_cloud::AFServer; +use serde_json::{Map, Value}; +use std::sync::Arc; +use tracing::{error, instrument}; +use uuid::Uuid; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, @@ -35,12 +34,10 @@ where #[allow(clippy::blocks_in_conditions)] async fn get_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { - let workspace_id = workspace_id.to_string(); - let object_id = object_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { @@ -51,7 +48,7 @@ where match result { Ok(data) => { check_request_workspace_id_is_match( - &workspace_id, + workspace_id, &cloned_user, format!("get database object: {}:{}", object_id, collab_type), )?; @@ -71,17 +68,17 @@ where #[allow(clippy::blocks_in_conditions)] async fn create_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - workspace_id: &str, + workspace_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let encoded_collab_v1 = encoded_collab .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?; let params = CreateCollabParams { - workspace_id: workspace_id.to_string(), - object_id: object_id.to_string(), + workspace_id: workspace_id.clone(), + object_id: object_id.clone(), encoded_collab_v1, collab_type, }; @@ -92,11 +89,10 @@ where #[instrument(level = "debug", skip_all)] async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let client = try_get_client?; @@ -104,8 +100,8 @@ where .into_iter() .map(|object_id| QueryCollab::new(object_id, object_ty)) .collect(); - let results = client.batch_get_collab(&workspace_id, params).await?; - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "batch get database object")?; + let results = client.batch_get_collab(workspace_id, params).await?; + check_request_workspace_id_is_match(workspace_id, &cloned_user, "batch get database object")?; Ok( results .0 @@ -131,8 +127,8 @@ where async fn get_database_collab_object_snapshots( &self, - _object_id: &str, - _limit: usize, + object_id: &Uuid, + limit: usize, ) -> Result, FlowyError> { Ok(vec![]) } @@ -145,17 +141,17 @@ where { async fn summary_database_row( &self, - workspace_id: &str, - _object_id: &str, - summary_row: SummaryRowContent, + workspace_id: &Uuid, + _object_id: &Uuid, + _summary_row: SummaryRowContent, ) -> Result { let try_get_client = self.inner.try_get_client(); - let map: Map = summary_row + let map: Map = _summary_row .into_iter() .map(|(key, value)| (key, Value::String(value))) .collect(); let params = SummarizeRowParams { - workspace_id: workspace_id.to_string(), + workspace_id: workspace_id.clone(), data: SummarizeRowData::Content(map), }; let data = try_get_client?.summarize_row(params).await?; @@ -164,19 +160,21 @@ where async fn translate_database_row( &self, - workspace_id: &str, - translate_row: TranslateRowContent, - language: &str, + workspace_id: &Uuid, + _translate_row: TranslateRowContent, + _language: &str, ) -> Result { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let data = TranslateRowData { - cells: translate_row, - language: language.to_string(), + cells: _translate_row, + language: _language.to_string(), include_header: false, }; - let params = TranslateRowParams { workspace_id, data }; + let params = TranslateRowParams { + workspace_id: workspace_id.to_string(), + data, + }; let data = try_get_client?.translate_row(params).await?; Ok(data) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index d73bbe4c75..e5e271b1e5 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -5,12 +5,12 @@ use collab::entity::EncodedCollab; use collab::preclude::Collab; use collab_document::document::Document; use collab_entity::CollabType; -use std::sync::Arc; -use tracing::instrument; - use flowy_document_pub::cloud::*; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use std::sync::Arc; +use tracing::instrument; +use uuid::Uuid; use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; @@ -29,12 +29,12 @@ where #[instrument(level = "debug", skip_all, fields(document_id = %document_id))] async fn get_document_doc_state( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: workspace_id.to_string(), - inner: QueryCollab::new(document_id.to_string(), CollabType::Document), + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.clone(), CollabType::Document), }; let doc_state = self .inner @@ -57,9 +57,9 @@ where async fn get_document_snapshots( &self, - _document_id: &str, - _limit: usize, - _workspace_id: &str, + document_id: &Uuid, + limit: usize, + workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } @@ -67,12 +67,12 @@ where #[instrument(level = "debug", skip_all)] async fn get_document_data( &self, - document_id: &str, - workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: workspace_id.to_string(), - inner: QueryCollab::new(document_id.to_string(), CollabType::Document), + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.clone(), CollabType::Document), }; let doc_state = self .inner @@ -89,7 +89,7 @@ where )?; let collab = Collab::new_with_source( CollabOrigin::Empty, - document_id, + document_id.to_string().as_str(), DataSource::DocStateV1(doc_state), vec![], false, @@ -100,13 +100,13 @@ where async fn create_document_collab( &self, - workspace_id: &str, - document_id: &str, + workspace_id: &Uuid, + document_id: &Uuid, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let params = CreateCollabParams { - workspace_id: workspace_id.to_string(), - object_id: document_id.to_string(), + workspace_id: workspace_id.clone(), + object_id: document_id.clone(), encoded_collab_v1: encoded_collab .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs index 9f4e15f430..8db806a0da 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs @@ -1,9 +1,10 @@ use crate::af_cloud::AFServer; use client_api::entity::{CompleteUploadRequest, CreateUploadRequest}; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub struct AFCloudFileStorageServiceImpl { pub client: T, @@ -56,10 +57,10 @@ where async fn get_object_url_v1( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, - ) -> Result { + ) -> FlowyResult { let url = self .client .try_get_client()? @@ -67,14 +68,14 @@ where Ok(url) } - async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)> { + async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { let value = self.client.try_get_client().ok()?.parse_blob_url_v1(url)?; Some(value) } async fn create_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, content_type: &str, @@ -109,7 +110,7 @@ where async fn upload_part( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -134,7 +135,7 @@ where async fn complete_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 5457164a87..3a912306fc 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -58,12 +58,10 @@ where }) } - async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); + async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { let try_get_client = self.inner.try_get_client(); - let client = try_get_client?; - let _ = client.open_workspace(&workspace_id).await?; + let _ = client.open_workspace(workspace_id).await?; Ok(()) } @@ -88,11 +86,10 @@ where #[instrument(level = "debug", skip_all)] async fn get_folder_data( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: &i64, ) -> Result, FlowyError> { let uid = *uid; - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { @@ -111,10 +108,10 @@ where uid, CollabOrigin::Empty, DataSource::DocStateV1(doc_state), - &workspace_id, + &workspace_id.to_string(), vec![], )?; - Ok(folder.get_folder_data(&workspace_id)) + Ok(folder.get_folder_data(&workspace_id.to_string())) } async fn get_folder_snapshots( @@ -128,18 +125,16 @@ where #[instrument(level = "debug", skip_all)] async fn get_folder_doc_state( &self, - workspace_id: &str, - _uid: i64, + workspace_id: &Uuid, + uid: i64, collab_type: CollabType, - object_id: &str, + object_id: &Uuid, ) -> Result, FlowyError> { - let object_id = object_id.to_string(); - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id, collab_type), + inner: QueryCollab::new(object_id.clone(), collab_type), }; let doc_state = try_get_client? .get_collab(params) @@ -154,10 +149,9 @@ where async fn full_sync_collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, params: FullSyncCollabParams, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); try_get_client? .collab_full_sync( @@ -173,10 +167,9 @@ where async fn batch_create_folder_collab_objects( &self, - workspace_id: &str, + workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = objects .into_iter() @@ -189,7 +182,7 @@ where }) .collect::>(); try_get_client? - .create_collab_list(&workspace_id, params) + .create_collab_list(workspace_id, params) .await?; Ok(()) } @@ -200,10 +193,9 @@ where async fn publish_view( &self, - workspace_id: &str, + workspace_id: &Uuid, payload: Vec, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = payload .into_iter() @@ -235,29 +227,20 @@ where async fn unpublish_views( &self, - workspace_id: &str, - view_ids: Vec, + workspace_id: &Uuid, + view_ids: Vec, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - let view_uuids = view_ids - .iter() - .map(|id| Uuid::parse_str(id).unwrap_or(Uuid::nil())) - .collect::>(); try_get_client? - .unpublish_collabs(&workspace_id, &view_uuids) + .unpublish_collabs(&workspace_id, &view_ids) .await?; Ok(()) } - async fn get_publish_info(&self, view_id: &str) -> Result { + async fn get_publish_info(&self, view_id: &Uuid) -> Result { let try_get_client = self.inner.try_get_client(); - let view_id = Uuid::parse_str(view_id) - .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id")); - - let view_id = view_id?; let info = try_get_client? - .get_published_collab_info(&view_id) + .get_published_collab_info(view_id) .await .map_err(FlowyError::from)?; Ok(info) @@ -265,14 +248,11 @@ where async fn set_publish_name( &self, - workspace_id: &str, - view_id: String, + workspace_id: &Uuid, + view_id: Uuid, new_name: String, ) -> Result<(), FlowyError> { let try_get_client = self.inner.try_get_client()?; - let view_id = Uuid::parse_str(&view_id) - .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id"))?; - try_get_client .patch_published_collabs( workspace_id, @@ -290,36 +270,33 @@ where async fn set_publish_namespace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_namespace: String, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); try_get_client? - .set_workspace_publish_namespace(&workspace_id, new_namespace) + .set_workspace_publish_namespace(workspace_id, new_namespace) .await?; Ok(()) } - async fn get_publish_namespace(&self, workspace_id: &str) -> Result { - let workspace_id = workspace_id.to_string(); + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { let namespace = self .inner .try_get_client()? - .get_workspace_publish_namespace(&workspace_id) + .get_workspace_publish_namespace(workspace_id) .await?; Ok(namespace) } async fn list_published_views( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { - let workspace_id = workspace_id.to_string(); let published_views = self .inner .try_get_client()? - .list_published_views(&workspace_id) + .list_published_views(workspace_id) .await .map_err(FlowyError::from)?; Ok(published_views) @@ -327,7 +304,7 @@ where async fn get_default_published_view_info( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { let default_published_view_info = self .inner @@ -340,7 +317,7 @@ where async fn set_default_published_view( &self, - workspace_id: &str, + workspace_id: &Uuid, view_id: uuid::Uuid, ) -> Result<(), FlowyError> { self @@ -352,7 +329,7 @@ where Ok(()) } - async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError> { + async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { self .inner .try_get_client()? diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs index 552a94068a..35fa6b153a 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs @@ -2,6 +2,7 @@ use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use flowy_search_pub::cloud::SearchCloudService; use lib_infra::async_trait::async_trait; +use uuid::Uuid; use crate::af_cloud::AFServer; @@ -22,7 +23,7 @@ where { async fn document_search( &self, - workspace_id: &str, + workspace_id: &Uuid, query: String, ) -> Result, FlowyError> { let client = self.inner.try_get_client()?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 208281fc5f..45ba817bce 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use anyhow::anyhow; @@ -189,9 +190,8 @@ where Ok(profile) } - async fn open_workspace(&self, workspace_id: &str) -> Result { + async fn open_workspace(&self, workspace_id: &Uuid) -> Result { let try_get_client = self.server.try_get_client(); - let workspace_id = workspace_id.to_string(); let client = try_get_client?; let af_workspace = client.open_workspace(&workspace_id).await?; Ok(to_user_workspace(af_workspace)) @@ -222,17 +222,14 @@ where async fn patch_workspace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_workspace_name: Option<&str>, new_workspace_icon: Option<&str>, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let owned_workspace_id = workspace_id.to_owned(); + let workspace_id = workspace_id.to_owned(); let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); - let workspace_id: Uuid = owned_workspace_id - .parse() - .map_err(|_| ErrorCode::InvalidParams)?; let client = try_get_client?; client .patch_workspace(PatchWorkspaceParam { @@ -244,18 +241,17 @@ where Ok(()) } - async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let workspace_id_owned = workspace_id.to_owned(); let client = try_get_client?; - client.delete_workspace(&workspace_id_owned).await?; + client.delete_workspace(workspace_id).await?; Ok(()) } async fn invite_workspace_member( &self, invitee_email: String, - workspace_id: String, + workspace_id: Uuid, role: Role, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); @@ -300,11 +296,11 @@ where async fn remove_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); try_get_client? - .remove_workspace_members(workspace_id, vec![user_email]) + .remove_workspace_members(&workspace_id, vec![user_email]) .await?; Ok(()) } @@ -312,20 +308,20 @@ where async fn update_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, role: Role, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); try_get_client? - .update_workspace_member(workspace_id, changeset) + .update_workspace_member(&workspace_id, changeset) .await?; Ok(()) } async fn get_workspace_members( &self, - workspace_id: String, + workspace_id: Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let members = try_get_client? @@ -339,15 +335,12 @@ where async fn get_workspace_member( &self, - workspace_id: String, + workspace_id: Uuid, uid: i64, ) -> Result { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let query = QueryWorkspaceMember { - workspace_id: workspace_id.clone(), - uid, - }; + let query = QueryWorkspaceMember { workspace_id, uid }; let member = client.get_workspace_member(query).await?; Ok(from_af_workspace_member(member)) } @@ -355,17 +348,15 @@ where #[instrument(level = "debug", skip_all)] async fn get_user_awareness_doc_state( &self, - _uid: i64, - workspace_id: &str, - object_id: &str, + uid: i64, + workspace_id: &Uuid, + object_id: &Uuid, ) -> Result, FlowyError> { - let workspace_id = workspace_id.to_string(); - let object_id = object_id.to_string(); let try_get_client = self.server.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id, CollabType::UserAwareness), + inner: QueryCollab::new(object_id.clone(), CollabType::UserAwareness), }; let resp = try_get_client?.get_collab(params).await?; check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get user awareness object")?; @@ -389,9 +380,12 @@ where let try_get_client = self.server.try_get_client(); let collab_object = collab_object.clone(); let client = try_get_client?; + let workspace_id = Uuid::from_str(&collab_object.workspace_id)?; + let object_id = Uuid::from_str(&collab_object.object_id)?; + let params = CreateCollabParams { - workspace_id: collab_object.workspace_id, - object_id: collab_object.object_id, + workspace_id, + object_id, collab_type: collab_object.collab_type, encoded_collab_v1: data, }; @@ -401,33 +395,35 @@ where async fn batch_create_collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); let params = objects .into_iter() - .map(|object| { - CollabParams::new( - object.object_id, - u8::from(object.collab_type).into(), - object.encoded_collab, - ) + .flat_map(|object| { + Uuid::from_str(&object.object_id) + .and_then(|object_id| { + Ok(CollabParams::new( + object_id, + u8::from(object.collab_type).into(), + object.encoded_collab, + )) + }) + .ok() }) .collect::>(); try_get_client? - .create_collab_list(&workspace_id, params) + .create_collab_list(workspace_id, params) .await .map_err(FlowyError::from)?; Ok(()) } - async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let workspace_id = workspace_id.to_string(); let client = try_get_client?; - client.leave_workspace(&workspace_id).await?; + client.leave_workspace(workspace_id).await?; Ok(()) } @@ -454,14 +450,13 @@ where async fn get_workspace_member_info( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, ) -> Result { let try_get_client = self.server.try_get_client(); - let workspace_id = workspace_id.to_string(); let client = try_get_client?; let params = QueryWorkspaceMember { - workspace_id: workspace_id.to_string(), + workspace_id: workspace_id.clone(), uid, }; let member = client.get_workspace_member(params).await?; @@ -518,12 +513,12 @@ where async fn get_workspace_plan( &self, - workspace_id: String, + workspace_id: Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; let plans = client - .get_active_workspace_subscriptions(&workspace_id) + .get_active_workspace_subscriptions(&workspace_id.to_string()) .await?; Ok(plans) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index 4075a5b908..0d91de8412 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -2,21 +2,22 @@ use crate::af_cloud::define::ServerUser; use flowy_error::{FlowyError, FlowyResult}; use std::sync::Arc; use tracing::warn; +use uuid::Uuid; /// Validates the workspace_id provided in the request. /// It checks that the workspace_id from the request matches the current user's active workspace_id. /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( - expected_workspace_id: &str, + expected_workspace_id: &Uuid, user: &Arc, action: impl AsRef, ) -> FlowyResult<()> { let actual_workspace_id = user.workspace_id()?; - if expected_workspace_id != actual_workspace_id { + if expected_workspace_id != &actual_workspace_id { warn!( "{}, expect workspace_id: {}, actual workspace_id: {}", action.as_ref(), - expected_workspace_id, + expected_workspace_id.to_string(), actual_workspace_id ); diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 7dca08754b..73633216b3 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -9,6 +9,7 @@ use lib_infra::async_trait::async_trait; use serde_json::Value; use std::collections::HashMap; use std::path::Path; +use uuid::Uuid; pub(crate) struct DefaultChatCloudServiceImpl; @@ -16,104 +17,104 @@ pub(crate) struct DefaultChatCloudServiceImpl; impl ChatCloudService for DefaultChatCloudServiceImpl { async fn create_chat( &self, - _uid: &i64, - _workspace_id: &str, - _chat_id: &str, - _rag_ids: Vec, + uid: &i64, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn create_question( &self, - _workspace_id: &str, - _chat_id: &str, - _message: &str, - _message_type: ChatMessageType, - _metadata: &[ChatMessageMetadata], + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + message_type: ChatMessageType, + metadata: &[ChatMessageMetadata], ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn create_answer( &self, - _workspace_id: &str, - _chat_id: &str, - _message: &str, - _question_id: i64, - _metadata: Option, + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + question_id: i64, + metadata: Option, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn stream_answer( &self, - _workspace_id: &str, - _chat_id: &str, - _message_id: i64, - _format: ResponseFormat, - _ai_model: Option, + workspace_id: &Uuid, + chat_id: &Uuid, + message_id: i64, + format: ResponseFormat, + ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_chat_messages( &self, - _workspace_id: &str, - _chat_id: &str, - _offset: MessageCursor, - _limit: u64, + workspace_id: &Uuid, + chat_id: &Uuid, + offset: MessageCursor, + limit: u64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_question_from_answer_id( &self, - _workspace_id: &str, - _chat_id: &str, - _answer_id: i64, + workspace_id: &Uuid, + chat_id: &Uuid, + answer_message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_related_message( &self, - _workspace_id: &str, - _chat_id: &str, - _message_id: i64, + workspace_id: &Uuid, + chat_id: &Uuid, + message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_answer( &self, - _workspace_id: &str, - _chat_id: &str, - _question_message_id: i64, + workspace_id: &Uuid, + chat_id: &Uuid, + question_message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn stream_complete( &self, - _workspace_id: &str, - _params: CompleteTextParams, - _ai_model: Option, + workspace_id: &Uuid, + params: CompleteTextParams, + ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) } async fn embed_file( &self, - _workspace_id: &str, - _file_path: &Path, - _chat_id: &str, - _metadata: Option>, + workspace_id: &Uuid, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_local_ai_config(&self, _workspace_id: &str) -> Result { + async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { Err( FlowyError::not_support() .with_context("Get local ai config is not supported in local server."), @@ -122,7 +123,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { async fn get_workspace_plan( &self, - _workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { Err( FlowyError::not_support() @@ -132,26 +133,26 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { async fn get_chat_settings( &self, - _workspace_id: &str, - _chat_id: &str, + workspace_id: &Uuid, + chat_id: &Uuid, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn update_chat_settings( &self, - _workspace_id: &str, - _chat_id: &str, - _params: UpdateChatParams, + workspace_id: &Uuid, + chat_id: &Uuid, + params: UpdateChatParams, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } - async fn get_available_models(&self, _workspace_id: &str) -> Result { + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } - async fn get_workspace_default_model(&self, _workspace_id: &str) -> Result { + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index d22088a2c4..36d31902f0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -7,6 +7,7 @@ use collab_user::core::default_user_awareness_data; use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub(crate) struct LocalServerDatabaseCloudServiceImpl(); @@ -14,50 +15,51 @@ pub(crate) struct LocalServerDatabaseCloudServiceImpl(); impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { async fn get_database_encode_collab( &self, - object_id: &str, + object_id: &Uuid, collab_type: CollabType, - _workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { + let object_id = object_id.to_string(); match collab_type { CollabType::Document => { - let encode_collab = default_document_collab_data(object_id)?; + let encode_collab = default_document_collab_data(&object_id)?; Ok(Some(encode_collab)) }, - CollabType::Database => default_database_data(object_id) + CollabType::Database => default_database_data(&object_id) .await .map(Some) .map_err(Into::into), - CollabType::WorkspaceDatabase => Ok(Some(default_workspace_database_data(object_id))), + CollabType::WorkspaceDatabase => Ok(Some(default_workspace_database_data(&object_id))), CollabType::Folder => Ok(None), CollabType::DatabaseRow => Ok(None), - CollabType::UserAwareness => Ok(Some(default_user_awareness_data(object_id))), + CollabType::UserAwareness => Ok(Some(default_user_awareness_data(&object_id))), CollabType::Unknown => Ok(None), } } async fn create_database_encode_collab( &self, - _object_id: &str, - _collab_type: CollabType, - _workspace_id: &str, - _encoded_collab: EncodedCollab, + object_id: &Uuid, + collab_type: CollabType, + workspace_id: &Uuid, + encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } async fn batch_get_database_encode_collab( &self, - _object_ids: Vec, - _object_ty: CollabType, - _workspace_id: &str, + object_ids: Vec, + object_ty: CollabType, + workspace_id: &Uuid, ) -> Result { Ok(EncodeCollabByOid::default()) } async fn get_database_collab_object_snapshots( &self, - _object_id: &str, - _limit: usize, + object_id: &Uuid, + limit: usize, ) -> Result, FlowyError> { Ok(vec![]) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index 152dcb78d8..fe44470921 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -2,6 +2,7 @@ use collab::entity::EncodedCollab; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub(crate) struct LocalServerDocumentCloudServiceImpl(); @@ -9,8 +10,8 @@ pub(crate) struct LocalServerDocumentCloudServiceImpl(); impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { async fn get_document_doc_state( &self, - document_id: &str, - _workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { let document_id = document_id.to_string(); @@ -22,26 +23,26 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { async fn get_document_snapshots( &self, - _document_id: &str, - _limit: usize, - _workspace_id: &str, + document_id: &Uuid, + limit: usize, + workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } async fn get_document_data( &self, - _document_id: &str, - _workspace_id: &str, + document_id: &Uuid, + workspace_id: &Uuid, ) -> Result, FlowyError> { Ok(None) } async fn create_document_collab( &self, - _workspace_id: &str, - _document_id: &str, - _encoded_collab: EncodedCollab, + workspace_id: &Uuid, + document_id: &Uuid, + encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 52d9a9e98c..a90352a976 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,10 +1,9 @@ use std::sync::Arc; +use crate::local_server::LocalServerDB; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab_entity::CollabType; - -use crate::local_server::LocalServerDB; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, @@ -12,6 +11,7 @@ use flowy_folder_pub::cloud::{ }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; +use uuid::Uuid; pub(crate) struct LocalServerFolderCloudServiceImpl { #[allow(dead_code)] @@ -29,7 +29,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { )) } - async fn open_workspace(&self, _workspace_id: &str) -> Result<(), FlowyError> { + async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { Ok(()) } @@ -39,8 +39,8 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn get_folder_data( &self, - _workspace_id: &str, - _uid: &i64, + workspace_id: &Uuid, + uid: &i64, ) -> Result, FlowyError> { Ok(None) } @@ -55,18 +55,18 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn get_folder_doc_state( &self, - _workspace_id: &str, - _uid: i64, - _collab_type: CollabType, - _object_id: &str, + workspace_id: &Uuid, + uid: i64, + collab_type: CollabType, + object_id: &Uuid, ) -> Result, FlowyError> { Err(FlowyError::local_version_not_support()) } async fn batch_create_folder_collab_objects( &self, - _workspace_id: &str, - _objects: Vec, + workspace_id: &Uuid, + objects: Vec, ) -> Result<(), FlowyError> { Ok(()) } @@ -77,68 +77,68 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn publish_view( &self, - _workspace_id: &str, - _payload: Vec, + workspace_id: &Uuid, + payload: Vec, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } async fn unpublish_views( &self, - _workspace_id: &str, - _view_ids: Vec, + workspace_id: &Uuid, + view_ids: Vec, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } - async fn get_publish_info(&self, _view_id: &str) -> Result { + async fn get_publish_info(&self, view_id: &Uuid) -> Result { Err(FlowyError::local_version_not_support()) } async fn set_publish_namespace( &self, - _workspace_id: &str, - _new_namespace: String, + workspace_id: &Uuid, + new_namespace: String, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } - async fn get_publish_namespace(&self, _workspace_id: &str) -> Result { + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { Err(FlowyError::local_version_not_support()) } async fn set_publish_name( &self, - _workspace_id: &str, - _view_id: String, - _new_name: String, + workspace_id: &Uuid, + view_id: Uuid, + new_name: String, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } async fn list_published_views( &self, - _workspace_id: &str, + workspace_id: &Uuid, ) -> Result, FlowyError> { Err(FlowyError::local_version_not_support()) } async fn get_default_published_view_info( &self, - _workspace_id: &str, + workspace_id: &Uuid, ) -> Result { Err(FlowyError::local_version_not_support()) } async fn set_default_published_view( &self, - _workspace_id: &str, - _view_id: uuid::Uuid, + workspace_id: &Uuid, + view_id: uuid::Uuid, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } - async fn remove_default_published_view(&self, _workspace_id: &str) -> Result<(), FlowyError> { + async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } @@ -148,8 +148,8 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn full_sync_collab_object( &self, - _workspace_id: &str, - _params: FullSyncCollabParams, + workspace_id: &Uuid, + params: FullSyncCollabParams, ) -> Result<(), FlowyError> { Ok(()) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index c800cc7ced..ddea5353d8 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -134,7 +134,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { } } - async fn open_workspace(&self, _workspace_id: &str) -> Result { + async fn open_workspace(&self, workspace_id: &Uuid) -> Result { Err( FlowyError::local_version_not_support() .with_context("local server doesn't support open workspace"), @@ -147,11 +147,16 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { async fn get_user_awareness_doc_state( &self, - _uid: i64, - _workspace_id: &str, - object_id: &str, + uid: i64, + workspace_id: &Uuid, + object_id: &Uuid, ) -> Result, FlowyError> { - let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); + let collab = Collab::new_with_origin( + CollabOrigin::Empty, + object_id.to_string().as_str(), + vec![], + false, + ); let awareness = UserAwareness::create(collab, None)?; let encode_collab = awareness.encode_collab_v1(|_collab| Ok::<_, FlowyError>(()))?; Ok(encode_collab.doc_state.to_vec()) @@ -171,8 +176,8 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { async fn batch_create_collab_object( &self, - _workspace_id: &str, - _objects: Vec, + workspace_id: &Uuid, + objects: Vec, ) -> Result<(), FlowyError> { Err( FlowyError::local_version_not_support() @@ -187,7 +192,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { ) } - async fn delete_workspace(&self, _workspace_id: &str) -> Result<(), FlowyError> { + async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { Err( FlowyError::local_version_not_support() .with_context("local server doesn't support multiple workspaces"), @@ -196,9 +201,9 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { async fn patch_workspace( &self, - _workspace_id: &str, - _new_workspace_name: Option<&str>, - _new_workspace_icon: Option<&str>, + workspace_id: &Uuid, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, ) -> Result<(), FlowyError> { Err( FlowyError::local_version_not_support() diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index ecf34ec31d..9c88917df8 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -39,7 +39,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc struct FakeServerUserImpl; impl ServerUser for FakeServerUserImpl { - fn workspace_id(&self) -> FlowyResult { + fn workspace_id(&self) -> FlowyResult { todo!() } } diff --git a/frontend/rust-lib/flowy-storage-pub/Cargo.toml b/frontend/rust-lib/flowy-storage-pub/Cargo.toml index ecab2212f8..d36c997432 100644 --- a/frontend/rust-lib/flowy-storage-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-storage-pub/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] lib-infra.workspace = true -serde_json.workspace = true serde.workspace = true async-trait.workspace = true mime = "0.3.17" @@ -17,4 +16,4 @@ mime_guess = "2.0.4" client-api-entity = { workspace = true } tokio = { workspace = true, features = ["sync", "io-util"] } anyhow = "1.0.86" -tracing.workspace = true +uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs index 6f12779899..5a72262ac9 100644 --- a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use mime::Mime; +use uuid::Uuid; #[async_trait] pub trait StorageCloudService: Send + Sync { @@ -47,17 +48,17 @@ pub trait StorageCloudService: Send + Sync { async fn get_object(&self, url: String) -> Result; async fn get_object_url_v1( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, ) -> FlowyResult; /// Return workspace_id, parent_dir, file_id - async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)>; + async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)>; async fn create_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, file_id: &str, content_type: &str, @@ -66,7 +67,7 @@ pub trait StorageCloudService: Send + Sync { async fn upload_part( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -76,7 +77,7 @@ pub trait StorageCloudService: Send + Sync { async fn complete_upload( &self, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -85,7 +86,7 @@ pub trait StorageCloudService: Send + Sync { } pub struct ObjectIdentity { - pub workspace_id: String, + pub workspace_id: Uuid, pub file_id: String, pub ext: String, } @@ -97,7 +98,7 @@ pub struct ObjectValue { } pub struct StorageObject { - pub workspace_id: String, + pub workspace_id: Uuid, pub file_name: String, pub value: ObjectValueSupabase, } @@ -126,9 +127,9 @@ impl StorageObject { /// * `name`: The name of the storage object. /// * `file_path`: The file path to the storage object's data. /// - pub fn from_file(workspace_id: &str, file_name: &str, file_path: T) -> Self { + pub fn from_file(workspace_id: &Uuid, file_name: &str, file_path: T) -> Self { Self { - workspace_id: workspace_id.to_string(), + workspace_id: *workspace_id, file_name: file_name.to_string(), value: ObjectValueSupabase::File { file_path: file_path.to_string(), @@ -145,14 +146,14 @@ impl StorageObject { /// * `mime`: The MIME type of the storage object. /// pub fn from_bytes>( - workspace_id: &str, + workspace_id: &Uuid, file_name: &str, bytes: B, mime: String, ) -> Self { let bytes = bytes.into(); Self { - workspace_id: workspace_id.to_string(), + workspace_id: *workspace_id, file_name: file_name.to_string(), value: ObjectValueSupabase::Bytes { bytes, mime }, } diff --git a/frontend/rust-lib/flowy-storage/Cargo.toml b/frontend/rust-lib/flowy-storage/Cargo.toml index c57cfde484..add7996439 100644 --- a/frontend/rust-lib/flowy-storage/Cargo.toml +++ b/frontend/rust-lib/flowy-storage/Cargo.toml @@ -17,8 +17,6 @@ tokio = { workspace = true, features = ["sync", "io-util"] } tracing.workspace = true flowy-sqlite.workspace = true mime_guess = "2.0.4" -fxhash = "0.2.1" -anyhow = "1.0.86" chrono = "0.4.33" flowy-notification = { workspace = true } flowy-derive.workspace = true @@ -26,8 +24,8 @@ protobuf = { workspace = true } dashmap.workspace = true strum_macros = "0.25.2" allo-isolate = { version = "^0.1", features = ["catch-unwind"] } -futures-util = "0.3.30" collab-importer = { workspace = true } +uuid.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index 66ad44e0fd..dc1bf053ea 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -24,15 +24,17 @@ use lib_infra::box_any::BoxAny; use lib_infra::isolate_stream::{IsolateSink, SinkExt}; use lib_infra::util::timestamp; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::atomic::AtomicBool; use std::sync::Arc; use tokio::io::AsyncWriteExt; use tokio::sync::{broadcast, watch}; use tracing::{debug, error, info, instrument, trace}; +use uuid::Uuid; pub trait StorageUserService: Send + Sync + 'static { fn user_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn get_application_root_dir(&self) -> &str; } @@ -157,7 +159,8 @@ impl StorageManager { let uid = self.user_service.user_id().ok()?; let mut conn = self.user_service.sqlite_connection(uid).ok()?; - let is_finish = is_upload_completed(&mut conn, &workspace_id, &parent_dir, &file_id).ok()?; + let is_finish = + is_upload_completed(&mut conn, &workspace_id.to_string(), &parent_dir, &file_id).ok()?; if let Err(err) = self.global_notifier.send(FileProgress::new_progress( url.to_string(), @@ -229,7 +232,7 @@ async fn prepare_upload_task( if let Ok(uid) = user_service.user_id() { let workspace_id = user_service.workspace_id()?; let conn = user_service.sqlite_connection(uid)?; - let upload_files = batch_select_upload_file(conn, &workspace_id, 100, false)?; + let upload_files = batch_select_upload_file(conn, &workspace_id.to_string(), 100, false)?; let tasks = upload_files .into_iter() .map(|upload_file| UploadTask::BackgroundTask { @@ -269,7 +272,7 @@ impl StorageService for StorageServiceImpl { self .task_queue - .remove_task(&workspace_id, &parent_dir, &file_id) + .remove_task(&workspace_id.to_string(), &parent_dir, &file_id) .await; trace!("[File] delete progress notifier: {}", file_id); @@ -278,7 +281,7 @@ impl StorageService for StorageServiceImpl { self .user_service .sqlite_connection(self.user_service.user_id()?)?, - &workspace_id, + &workspace_id.to_string(), &parent_dir, &file_id, ) { @@ -384,9 +387,10 @@ impl StorageService for StorageServiceImpl { let conn = self .user_service .sqlite_connection(self.user_service.user_id()?)?; + let workspace_id = Uuid::from_str(&record.workspace_id)?; let url = self .cloud_service - .get_object_url_v1(&record.workspace_id, &record.parent_dir, &record.file_id) + .get_object_url_v1(&workspace_id, &record.parent_dir, &record.file_id) .await?; let file_id = record.file_id.clone(); match insert_upload_file(conn, &record) { @@ -478,7 +482,8 @@ impl StorageService for StorageServiceImpl { .user_service .sqlite_connection(self.user_service.user_id()?)?; let workspace_id = self.user_service.workspace_id()?; - is_upload_completed(&mut conn, &workspace_id, parent_idr, file_id).unwrap_or(false) + is_upload_completed(&mut conn, &workspace_id.to_string(), parent_idr, file_id) + .unwrap_or(false) }; if is_completed { @@ -590,9 +595,10 @@ async fn start_upload( upload_file.file_id ); + let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; let create_upload_resp_result = cloud_service .create_upload( - &upload_file.workspace_id, + &workspace_id, &upload_file.parent_dir, &upload_file.file_id, &upload_file.content_type, @@ -601,11 +607,7 @@ async fn start_upload( .await; let file_url = cloud_service - .get_object_url_v1( - &upload_file.workspace_id, - &upload_file.parent_dir, - &upload_file.file_id, - ) + .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) .await?; if let Err(err) = create_upload_resp_result.as_ref() { @@ -653,7 +655,7 @@ async fn start_upload( match upload_part( cloud_service, user_service, - &upload_file.workspace_id, + &workspace_id, &upload_file.parent_dir, &upload_file.upload_id, &upload_file.file_id, @@ -782,7 +784,7 @@ async fn resume_upload( async fn upload_part( cloud_service: &Arc, user_service: &Arc, - workspace_id: &str, + workspace_id: &Uuid, parent_dir: &str, upload_id: &str, file_id: &str, @@ -822,12 +824,9 @@ async fn complete_upload( parts: Vec, global_notifier: &GlobalNotifier, ) -> Result<(), FlowyError> { + let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; let file_url = cloud_service - .get_object_url_v1( - &upload_file.workspace_id, - &upload_file.parent_dir, - &upload_file.file_id, - ) + .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) .await?; info!( @@ -838,7 +837,7 @@ async fn complete_upload( ); match cloud_service .complete_upload( - &upload_file.workspace_id, + &workspace_id, &upload_file.parent_dir, &upload_file.upload_id, &upload_file.file_id, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index d68bf3f809..b549c549fc 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -171,7 +171,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// return None if the user is not found async fn get_user_profile(&self, credential: UserCredentials) -> Result; - async fn open_workspace(&self, workspace_id: &str) -> Result; + async fn open_workspace(&self, workspace_id: &Uuid) -> Result; /// Return the all the workspaces of the user async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError>; @@ -183,18 +183,18 @@ pub trait UserCloudService: Send + Sync + 'static { // Updates the workspace name and icon async fn patch_workspace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_workspace_name: Option<&str>, new_workspace_icon: Option<&str>, ) -> Result<(), FlowyError>; /// Deletes a workspace owned by the user. - async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>; + async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; async fn invite_workspace_member( &self, invitee_email: String, - workspace_id: String, + workspace_id: Uuid, role: Role, ) -> Result<(), FlowyError> { Ok(()) @@ -214,7 +214,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn remove_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, ) -> Result<(), FlowyError> { Ok(()) } @@ -222,7 +222,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn update_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, role: Role, ) -> Result<(), FlowyError> { Ok(()) @@ -230,14 +230,14 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_members( &self, - workspace_id: String, + workspace_id: Uuid, ) -> Result, FlowyError> { Ok(vec![]) } async fn get_workspace_member( &self, - workspace_id: String, + workspace_id: Uuid, uid: i64, ) -> Result { Err(FlowyError::not_support()) @@ -246,8 +246,8 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_user_awareness_doc_state( &self, uid: i64, - workspace_id: &str, - object_id: &str, + workspace_id: &Uuid, + object_id: &Uuid, ) -> Result, FlowyError>; fn receive_realtime_event(&self, _json: Value) {} @@ -266,11 +266,11 @@ pub trait UserCloudService: Send + Sync + 'static { async fn batch_create_collab_object( &self, - workspace_id: &str, + workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError>; - async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { Ok(()) } @@ -286,7 +286,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_member_info( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, ) -> Result { Err(FlowyError::not_support()) @@ -318,7 +318,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_plan( &self, - workspace_id: String, + workspace_id: Uuid, ) -> Result, FlowyError> { Err(FlowyError::not_support()) } diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 8f31edf740..857a735edb 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use chrono::{DateTime, Utc}; pub use client_api::entity::billing_dto::RecurringInterval; use client_api::entity::AFRole; +use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_repr::*; @@ -151,6 +152,11 @@ pub struct UserWorkspace { } impl UserWorkspace { + pub fn workspace_id(&self) -> FlowyResult { + let id = Uuid::from_str(&self.id)?; + Ok(id) + } + pub fn new_local(workspace_id: &str, _uid: i64) -> Self { Self { id: workspace_id.to_string(), diff --git a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs index 2b1b58ae05..122ce2e60f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs +++ b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs @@ -3,6 +3,7 @@ use flowy_error::FlowyResult; use flowy_folder_pub::entities::ImportFrom; use lib_infra::async_trait::async_trait; use std::collections::HashMap; +use uuid::Uuid; #[async_trait] pub trait UserWorkspaceService: Send + Sync { @@ -19,5 +20,5 @@ pub trait UserWorkspaceService: Send + Sync { ) -> FlowyResult<()>; /// Removes local indexes when a workspace is left/deleted - fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()>; + fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index c22af4f1c1..dfd166b1a6 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -5,9 +5,11 @@ use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use serde_json::Value; +use std::str::FromStr; use std::sync::Weak; use std::{convert::TryInto, sync::Arc}; use tracing::{event, trace}; +use uuid::Uuid; use crate::entities::*; use crate::notification::{send_notification, UserNotification}; @@ -515,7 +517,8 @@ pub async fn open_workspace_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - manager.open_workspace(¶ms.workspace_id).await?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; + manager.open_workspace(&workspace_id).await?; Ok(()) } @@ -645,8 +648,9 @@ pub async fn delete_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .remove_workspace_member(data.email, data.workspace_id) + .remove_workspace_member(data.email, workspace_id) .await?; Ok(()) } @@ -658,8 +662,9 @@ pub async fn get_workspace_members_handler( ) -> DataResult { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(&data.workspace_id)?; let members = manager - .get_workspace_members(data.workspace_id) + .get_workspace_members(workspace_id) .await? .into_iter() .map(WorkspaceMemberPB::from) @@ -674,8 +679,9 @@ pub async fn update_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .update_workspace_member(data.email, data.workspace_id, data.role.into()) + .update_workspace_member(data.email, workspace_id, data.role.into()) .await?; Ok(()) } @@ -698,6 +704,7 @@ pub async fn delete_workspace_handler( ) -> Result<(), FlowyError> { let workspace_id = delete_workspace_param.try_into_inner()?.workspace_id; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(&workspace_id)?; manager.delete_workspace(&workspace_id).await?; Ok(()) } @@ -709,8 +716,9 @@ pub async fn rename_workspace_handler( ) -> Result<(), FlowyError> { let params = rename_workspace_param.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; manager - .patch_workspace(¶ms.workspace_id, Some(¶ms.new_name), None) + .patch_workspace(&workspace_id, Some(¶ms.new_name), None) .await?; Ok(()) } @@ -722,8 +730,9 @@ pub async fn change_workspace_icon_handler( ) -> Result<(), FlowyError> { let params = change_workspace_icon_param.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; manager - .patch_workspace(¶ms.workspace_id, None, Some(¶ms.new_icon)) + .patch_workspace(&workspace_id, None, Some(¶ms.new_icon)) .await?; Ok(()) } @@ -735,8 +744,9 @@ pub async fn invite_workspace_member_handler( ) -> Result<(), FlowyError> { let param = param.try_into_inner()?; let manager = upgrade_manager(manager)?; + let workspace_id = Uuid::from_str(¶m.workspace_id)?; manager - .invite_member_to_workspace(param.workspace_id, param.invitee_email, param.role.into()) + .invite_member_to_workspace(workspace_id, param.invitee_email, param.role.into()) .await?; Ok(()) } @@ -772,6 +782,7 @@ pub async fn leave_workspace_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let workspace_id = param.into_inner().workspace_id; + let workspace_id = Uuid::from_str(&workspace_id)?; let manager = upgrade_manager(manager)?; manager.leave_workspace(&workspace_id).await?; Ok(()) diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 7d770e8123..84d43fe4f2 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -13,8 +13,10 @@ use flowy_sqlite::DBConnection; use flowy_user_pub::entities::UserWorkspace; use flowy_user_pub::session::Session; use std::path::PathBuf; +use std::str::FromStr; use std::sync::{Arc, Weak}; use tracing::{error, info}; +use uuid::Uuid; const SQLITE_VACUUM_042: &str = "sqlite_vacuum_042_version"; @@ -68,14 +70,16 @@ impl AuthenticateUser { Ok(self.user_config.device_id.to_string()) } - pub fn workspace_id(&self) -> FlowyResult { + pub fn workspace_id(&self) -> FlowyResult { let session = self.get_session()?; - Ok(session.user_workspace.id.clone()) + let workspace_uuid = Uuid::from_str(&session.user_workspace.id)?; + Ok(workspace_uuid) } - pub fn workspace_database_object_id(&self) -> FlowyResult { + pub fn workspace_database_object_id(&self) -> FlowyResult { let session = self.get_session()?; - Ok(session.user_workspace.workspace_database_id.clone()) + let id = Uuid::from_str(&session.user_workspace.workspace_database_id)?; + Ok(id) } pub fn get_collab_db(&self, uid: i64) -> FlowyResult> { diff --git a/frontend/rust-lib/flowy-user/src/services/billing_check.rs b/frontend/rust-lib/flowy-user/src/services/billing_check.rs index b0b123fc41..b15f811ab6 100644 --- a/frontend/rust-lib/flowy-user/src/services/billing_check.rs +++ b/frontend/rust-lib/flowy-user/src/services/billing_check.rs @@ -4,6 +4,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_user_pub::cloud::UserCloudServiceProvider; use std::sync::Weak; use std::time::Duration; +use uuid::Uuid; /// `PeriodicallyCheckBillingState` is designed to periodically verify the subscription /// plan of a given workspace. It utilizes a cloud service provider to fetch the current @@ -13,7 +14,7 @@ use std::time::Duration; /// at specified intervals until the expected plan is found or the maximum number of /// attempts is reached. pub struct PeriodicallyCheckBillingState { - workspace_id: String, + workspace_id: Uuid, cloud_service: Weak, expected_plan: Option, user: Weak, @@ -21,7 +22,7 @@ pub struct PeriodicallyCheckBillingState { impl PeriodicallyCheckBillingState { pub fn new( - workspace_id: String, + workspace_id: Uuid, expected_plan: Option, cloud_service: Weak, user: Weak, diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index c2325815d6..9efbf81932 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -43,6 +43,8 @@ use std::ops::{Deref, DerefMut}; use std::path::Path; use std::sync::{Arc, Weak}; use tracing::{error, event, info, instrument, warn}; +use uuid::Uuid; + pub(crate) struct ImportedFolder { pub imported_session: Session, pub imported_collab_db: Arc, @@ -1172,7 +1174,7 @@ impl DerefMut for OldToNewIdMap { pub async fn upload_collab_objects_data( uid: i64, user_collab_db: Weak, - workspace_id: &str, + workspace_id: &Uuid, user_authenticator: &Authenticator, collab_data: ImportedCollabData, user_cloud_service: Arc, @@ -1275,7 +1277,7 @@ pub async fn upload_collab_objects_data( async fn batch_create( uid: i64, - workspace_id: &str, + workspace_id: &Uuid, user_cloud_service: &Arc, size_counter: &usize, objects: Vec, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index f04c988f5c..c41485f6c9 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -14,6 +14,7 @@ use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudServiceProvider, UserUpdate}; use flowy_user_pub::entities::*; use flowy_user_pub::workspace_service::UserWorkspaceService; +use lib_infra::box_any::BoxAny; use semver::Version; use serde_json::Value; use std::string::ToString; @@ -22,8 +23,7 @@ use std::sync::{Arc, Weak}; use tokio::sync::Mutex; use tokio_stream::StreamExt; use tracing::{debug, error, event, info, instrument, warn}; - -use lib_infra::box_any::BoxAny; +use uuid::Uuid; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserStatusCallback}; @@ -58,7 +58,7 @@ pub struct UserManager { auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, - pub(crate) is_loading_awareness: Arc>, + pub(crate) is_loading_awareness: Arc>, } impl UserManager { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index d8c749ac0c..cc9cddabb5 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -8,13 +8,13 @@ use collab_entity::CollabType; use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, }; +use collab_integrate::CollabKVDB; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; use dashmap::try_result::TryResult; -use tracing::{error, info, instrument, trace}; - -use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; +use tracing::{error, info, instrument, trace}; +use uuid::Uuid; use crate::entities::ReminderPB; use crate::user_manager::UserManager; @@ -122,8 +122,7 @@ impl UserManager { authenticator: &Authenticator, ) -> FlowyResult<()> { let authenticator = authenticator.clone(); - let object_id = - user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); + let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); // Try to acquire mutable access to `is_loading_awareness`. // Thread-safety is ensured by DashMap @@ -156,7 +155,7 @@ impl UserManager { let is_exist_on_disk = self .authenticate_user - .is_collab_on_disk(session.user_id, &object_id)?; + .is_collab_on_disk(session.user_id, &object_id.to_string())?; if authenticator.is_local() || is_exist_on_disk { trace!( "Initializing new user awareness from disk:{}, {:?}", @@ -164,15 +163,13 @@ impl UserManager { authenticator ); let collab_db = self.get_collab_db(session.user_id)?; - let doc_state = CollabPersistenceImpl::new( - collab_db.clone(), - session.user_id, - session.user_workspace.id.clone(), - ) - .into_data_source(); + let workspace_id = session.user_workspace.workspace_id()?; + let doc_state = + CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) + .into_data_source(); let awareness = Self::collab_for_user_awareness( &self.collab_builder.clone(), - &session.user_workspace.id, + &workspace_id, session.user_id, &object_id, collab_db, @@ -211,7 +208,7 @@ impl UserManager { fn load_awareness_from_server( &self, session: &Session, - object_id: String, + object_id: Uuid, authenticator: Authenticator, ) -> FlowyResult<()> { // Clone necessary data @@ -231,16 +228,14 @@ impl UserManager { } }; + let workspace_id = session.user_workspace.workspace_id()?; let create_awareness = if authenticator.is_local() { - let doc_state = CollabPersistenceImpl::new( - collab_db.clone(), - session.user_id, - session.user_workspace.id.clone(), - ) - .into_data_source(); + let doc_state = + CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id.clone()) + .into_data_source(); Self::collab_for_user_awareness( &weak_builder, - &session.user_workspace.id, + &workspace_id, session.user_id, &object_id, collab_db, @@ -251,7 +246,7 @@ impl UserManager { } else { let result = cloud_services .get_user_service()? - .get_user_awareness_doc_state(session.user_id, &session.user_workspace.id, &object_id) + .get_user_awareness_doc_state(session.user_id, &workspace_id, &object_id) .await; match result { @@ -259,7 +254,7 @@ impl UserManager { trace!("Fetched user awareness collab from remote: {}", data.len()); Self::collab_for_user_awareness( &weak_builder, - &session.user_workspace.id, + &workspace_id, session.user_id, &object_id, collab_db, @@ -274,12 +269,12 @@ impl UserManager { let doc_state = CollabPersistenceImpl::new( collab_db.clone(), session.user_id, - session.user_workspace.id.clone(), + workspace_id.clone(), ) .into_data_source(); Self::collab_for_user_awareness( &weak_builder, - &session.user_workspace.id, + &workspace_id, session.user_id, &object_id, collab_db, @@ -329,9 +324,9 @@ impl UserManager { /// user awareness. async fn collab_for_user_awareness( collab_builder: &Weak, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, - object_id: &str, + object_id: &Uuid, collab_db: Weak, doc_state: DataSource, notifier: Option, @@ -375,8 +370,7 @@ impl UserManager { info!("User awareness is not loaded when trying to access it"); let session = self.get_session()?; - let object_id = - user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); + let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); let is_loading = self .is_loading_awareness .get(&object_id) diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index e666d2486a..3f3d90127d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -3,12 +3,11 @@ use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlanDetail} use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; use std::convert::TryFrom; +use std::str::FromStr; use std::sync::Arc; use collab_entity::{CollabObject, CollabType}; use collab_integrate::CollabKVDB; -use tracing::{error, info, instrument, trace, warn}; - use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; use flowy_sqlite::schema::user_workspace_table; @@ -17,6 +16,8 @@ use flowy_user_pub::entities::{ Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; +use tracing::{error, info, instrument, trace, warn}; +use uuid::Uuid; use crate::entities::{ RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, @@ -123,7 +124,7 @@ impl UserManager { match upload_collab_objects_data( user_id, weak_user_collab_db, - ¤t_session.user_workspace.id, + ¤t_session.user_workspace.workspace_id()?, &user.authenticator, collab_data, weak_user_cloud_service, @@ -161,7 +162,7 @@ impl UserManager { } #[instrument(skip(self), err)] - pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + pub async fn open_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("open workspace: {}", workspace_id); let user_workspace = self .cloud_services @@ -221,7 +222,7 @@ impl UserManager { pub async fn patch_workspace( &self, - workspace_id: &str, + workspace_id: &Uuid, new_workspace_name: Option<&str>, new_workspace_icon: Option<&str>, ) -> FlowyResult<()> { @@ -262,7 +263,7 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn leave_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + pub async fn leave_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("leave workspace: {}", workspace_id); self .cloud_services @@ -273,15 +274,15 @@ impl UserManager { // delete workspace from local sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id)?; + delete_user_workspaces(conn, workspace_id.to_string().as_str())?; self .user_workspace_service - .did_delete_workspace(workspace_id.to_string()) + .did_delete_workspace(workspace_id) } #[instrument(level = "info", skip(self), err)] - pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + pub async fn delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("delete workspace: {}", workspace_id); self .cloud_services @@ -290,18 +291,18 @@ impl UserManager { .await?; let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id)?; + delete_user_workspaces(conn, workspace_id.to_string().as_str())?; self .user_workspace_service - .did_delete_workspace(workspace_id.to_string())?; + .did_delete_workspace(workspace_id)?; Ok(()) } pub async fn invite_member_to_workspace( &self, - workspace_id: String, + workspace_id: Uuid, invitee_email: String, role: Role, ) -> FlowyResult<()> { @@ -335,7 +336,7 @@ impl UserManager { pub async fn remove_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, ) -> FlowyResult<()> { self .cloud_services @@ -347,7 +348,7 @@ impl UserManager { pub async fn get_workspace_members( &self, - workspace_id: String, + workspace_id: Uuid, ) -> FlowyResult> { let members = self .cloud_services @@ -359,7 +360,7 @@ impl UserManager { pub async fn get_workspace_member( &self, - workspace_id: String, + workspace_id: Uuid, uid: i64, ) -> FlowyResult { let member = self @@ -373,7 +374,7 @@ impl UserManager { pub async fn update_workspace_member( &self, user_email: String, - workspace_id: String, + workspace_id: Uuid, role: Role, ) -> FlowyResult<()> { self @@ -384,9 +385,9 @@ impl UserManager { Ok(()) } - pub fn get_user_workspace(&self, uid: i64, workspace_id: &str) -> Option { + pub fn get_user_workspace(&self, uid: i64, workspace_id: &Uuid) -> Option { let conn = self.db_connection(uid).ok()?; - get_user_workspace_op(workspace_id, conn) + get_user_workspace_op(workspace_id.to_string().as_str(), conn) } pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { @@ -581,10 +582,10 @@ impl UserManager { } pub async fn get_workspace_member_info(&self, uid: i64) -> FlowyResult { - let workspace_id = self.get_session()?.user_workspace.id.clone(); + let workspace_id = self.get_session()?.user_workspace.workspace_id()?; let db = self.authenticate_user.get_sqlite_connection(uid)?; // Can opt in using memory cache - if let Ok(member_record) = select_workspace_member(db, &workspace_id, uid) { + if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self .get_workspace_member_info_from_remote(&workspace_id, uid) @@ -608,7 +609,7 @@ impl UserManager { async fn get_workspace_member_info_from_remote( &self, - workspace_id: &str, + workspace_id: &Uuid, uid: i64, ) -> FlowyResult { trace!("get workspace member info from remote: {}", workspace_id); @@ -638,8 +639,9 @@ impl UserManager { success: SuccessWorkspaceSubscriptionPB, ) -> FlowyResult<()> { // periodically check the billing state + let workspace_id = Uuid::from_str(&success.workspace_id)?; let plans = PeriodicallyCheckBillingState::new( - success.workspace_id, + workspace_id, success.plan.map(SubscriptionPlan::from), Arc::downgrade(&self.cloud_services), Arc::downgrade(&self.authenticate_user), From 91397c963a4d63ead18828dc9f9f67bbc24b3b1e Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 7 Apr 2025 19:34:26 +0800 Subject: [PATCH 273/384] chore: clippy --- .../src/persistence/collab_metadata_sql.rs | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 29 +++--- frontend/rust-lib/flowy-ai/src/chat.rs | 18 ++-- frontend/rust-lib/flowy-ai/src/completion.rs | 2 +- .../flowy-ai/src/local_ai/controller.rs | 4 +- .../flowy-core/src/deps_resolve/chat_deps.rs | 2 +- .../src/deps_resolve/cloud_service_impl.rs | 8 +- .../folder_deps/folder_deps_chat_impl.rs | 16 ++-- .../folder_deps/folder_deps_database_impl.rs | 3 +- .../folder_deps/folder_deps_doc_impl.rs | 8 +- .../src/deps_resolve/folder_deps/mod.rs | 2 +- .../rust-lib/flowy-database2/src/manager.rs | 6 +- .../src/services/database/database_editor.rs | 2 +- .../rust-lib/flowy-document/src/manager.rs | 6 +- .../flowy-document/tests/document/util.rs | 25 +++-- .../flowy-folder/src/entities/view.rs | 6 +- frontend/rust-lib/flowy-folder/src/manager.rs | 10 +- .../rust-lib/flowy-folder/src/manager_init.rs | 24 ++--- .../flowy-folder/src/manager_observer.rs | 2 +- .../flowy-search/src/folder/indexer.rs | 12 +-- .../flowy-server/src/af_cloud/impls/chat.rs | 1 + .../src/af_cloud/impls/database.rs | 11 ++- .../src/af_cloud/impls/document.rs | 13 +-- .../flowy-server/src/af_cloud/impls/folder.rs | 22 ++--- .../af_cloud/impls/user/cloud_service_impl.rs | 18 ++-- .../rust-lib/flowy-server/src/default_impl.rs | 96 +++++++++---------- .../src/local_server/impls/database.rs | 1 + .../src/local_server/impls/document.rs | 1 + .../src/local_server/impls/folder.rs | 1 + .../src/local_server/impls/user.rs | 1 + .../flowy-user/src/services/billing_check.rs | 2 +- .../user_manager/manager_user_awareness.rs | 11 +-- 32 files changed, 181 insertions(+), 184 deletions(-) diff --git a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs index bc5f28e642..adb8b72de1 100644 --- a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs +++ b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs @@ -56,7 +56,7 @@ pub fn batch_select_collab_metadata( .filter(af_collab_metadata::object_id.eq_any(&object_ids)) .load::(&mut conn)? .into_iter() - .flat_map(|m| Uuid::from_str(&m.object_id).and_then(|v| Ok((v, m)))) + .flat_map(|m| Uuid::from_str(&m.object_id).map(|v| (v, m))) .collect(); Ok(metadata) } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index e11f2328e4..0b27f4f525 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -135,10 +135,10 @@ impl AIManager { } pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - self.chats.entry(chat_id.clone()).or_insert_with(|| { + self.chats.entry(*chat_id).or_insert_with(|| { Arc::new(Chat::new( self.user_service.user_id().unwrap(), - chat_id.clone(), + *chat_id, self.user_service.clone(), self.cloud_service_wm.clone(), )) @@ -152,7 +152,7 @@ impl AIManager { let cloud_service_wm = self.cloud_service_wm.clone(); let store_preferences = self.store_preferences.clone(); let external_service = self.external_service.clone(); - let chat_id = chat_id.clone(); + let chat_id = *chat_id; tokio::spawn(async move { match refresh_chat_setting( &user_service, @@ -238,11 +238,11 @@ impl AIManager { let chat = Arc::new(Chat::new( self.user_service.user_id()?, - chat_id.clone(), + *chat_id, self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(chat_id.clone(), chat.clone()); + self.chats.insert(*chat_id, chat.clone()); Ok(chat) } @@ -533,11 +533,11 @@ impl AIManager { None => { let chat = Arc::new(Chat::new( self.user_service.user_id()?, - chat_id.clone(), + *chat_id, self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(chat_id.clone(), chat.clone()); + self.chats.insert(*chat_id, chat.clone()); Ok(chat) }, Some(chat) => Ok(chat), @@ -739,18 +739,15 @@ async fn refresh_chat_setting( error!("failed to set chat settings: {}", err); } - chat_notification_builder( - &chat_id.to_string(), - ChatNotification::DidUpdateChatSettings, - ) - .payload(ChatSettingsPB { - rag_ids: settings.rag_ids.clone(), - }) - .send(); + chat_notification_builder(chat_id.to_string(), ChatNotification::DidUpdateChatSettings) + .payload(ChatSettingsPB { + rag_ids: settings.rag_ids.clone(), + }) + .send(); Ok(settings) } fn setting_store_key(chat_id: &Uuid) -> String { - format!("chat_settings_{}", chat_id.to_string()) + format!("chat_settings_{}", chat_id) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index d893ff2b51..44e2a1d41c 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -204,7 +204,7 @@ impl Chat { ai_model: Option, ) { let stop_stream = self.stop_stream.clone(); - let chat_id = self.chat_id.clone(); + let chat_id = self.chat_id; let cloud_service = self.chat_service.clone(); let user_service = self.user_service.clone(); tokio::spawn(async move { @@ -258,7 +258,7 @@ impl Chat { chat_id: chat_id.to_string(), error_message: err.to_string(), }; - chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) + chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) .payload(pb) .send(); return Err(err); @@ -297,14 +297,14 @@ impl Chat { chat_id: chat_id.to_string(), error_message: err.to_string(), }; - chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) + chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) .payload(pb) .send(); return Err(err); }, } - chat_notification_builder(&chat_id, ChatNotification::FinishStreaming).send(); + chat_notification_builder(chat_id, ChatNotification::FinishStreaming).send(); trace!("[Chat] finish streaming"); if answer_stream_buffer.lock().await.is_empty() { @@ -360,7 +360,7 @@ impl Chat { has_more: true, total: 0, }; - chat_notification_builder(&self.chat_id, ChatNotification::DidLoadPrevChatMessage) + chat_notification_builder(self.chat_id, ChatNotification::DidLoadPrevChatMessage) .payload(pb.clone()) .send(); return Ok(pb); @@ -433,7 +433,7 @@ impl Chat { after_message_id ); let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id.clone(); + let chat_id = self.chat_id; let cloud_service = self.chat_service.clone(); let user_service = self.user_service.clone(); let uid = self.uid; @@ -481,11 +481,11 @@ impl Chat { } else { *prev_message_state.write().await = PrevMessageState::NoMore; } - chat_notification_builder(&chat_id, ChatNotification::DidLoadPrevChatMessage) + chat_notification_builder(chat_id, ChatNotification::DidLoadPrevChatMessage) .payload(pb) .send(); } else { - chat_notification_builder(&chat_id, ChatNotification::DidLoadLatestChatMessage) + chat_notification_builder(chat_id, ChatNotification::DidLoadLatestChatMessage) .payload(pb) .send(); } @@ -511,7 +511,7 @@ impl Chat { } let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id.clone(); + let chat_id = self.chat_id; let cloud_service = self.chat_service.clone(); let question = cloud_service diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 53d2f3915e..31acde4ae7 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -130,7 +130,7 @@ impl CompletionTask { completion_type: Some(complete_type), metadata: Some(CompletionMetadata { object_id, - workspace_id: Some(self.workspace_id.clone()), + workspace_id: Some(self.workspace_id), rag_ids: Some(self.context.rag_ids), completion_history, custom_prompt: self diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 8b3285507c..6fbe7ce2fc 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -253,7 +253,7 @@ impl LocalAIController { self.close_chat(current_chat_id); } - self.current_chat_id.store(Some(Arc::new(chat_id.clone()))); + self.current_chat_id.store(Some(Arc::new(*chat_id))); let chat_id = chat_id.to_string(); let weak_ctrl = Arc::downgrade(&self.ai_plugin); tokio::spawn(async move { @@ -620,5 +620,5 @@ impl LLMResourceService for LLMResourceServiceImpl { const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; fn local_ai_enabled_key(workspace_id: &Uuid) -> String { - format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id.to_string()) + format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 5a369eb9de..2d9a57b331 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -112,7 +112,7 @@ impl AIExternalService for ChatQueryServiceImpl { // Perform full sync if changes are detected or no state vector is found let params = FullSyncCollabParams { - object_id: rag_id.clone(), + object_id: rag_id, collab_type: CollabType::Document, encoded_collab: query_collab.encoded_collab.clone(), }; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 896a03cfd5..32cf633a6f 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -444,7 +444,7 @@ impl DatabaseCloudService for ServerProvider { let server = self.get_server()?; server .database_service() - .get_database_encode_collab(object_id, collab_type, &workspace_id) + .get_database_encode_collab(object_id, collab_type, workspace_id) .await } @@ -485,7 +485,7 @@ impl DatabaseCloudService for ServerProvider { server .database_service() - .get_database_collab_object_snapshots(&object_id, limit) + .get_database_collab_object_snapshots(object_id, limit) .await } } @@ -686,7 +686,7 @@ impl ChatCloudService for ServerProvider { self .get_server()? .chat_service() - .create_question(&workspace_id, &chat_id, &message, message_type, metadata) + .create_question(workspace_id, chat_id, &message, message_type, metadata) .await } @@ -716,7 +716,7 @@ impl ChatCloudService for ServerProvider { let server = self.get_server()?; server .chat_service() - .stream_answer(&workspace_id, &chat_id, message_id, format, ai_model) + .stream_answer(workspace_id, chat_id, message_id, format, ai_model) .await } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs index 585cba84c5..fc7a861cb8 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs @@ -30,7 +30,7 @@ impl FolderOperationHandler for ChatFolderOperation { self.0.delete_chat(view_id).await } - async fn duplicate_view(&self, view_id: &Uuid) -> Result { + async fn duplicate_view(&self, _view_id: &Uuid) -> Result { Err(FlowyError::not_support()) } @@ -47,8 +47,8 @@ impl FolderOperationHandler for ChatFolderOperation { user_id: i64, parent_view_id: &Uuid, view_id: &Uuid, - name: &str, - layout: ViewLayout, + _name: &str, + _layout: ViewLayout, ) -> Result<(), FlowyError> { self .0 @@ -59,11 +59,11 @@ impl FolderOperationHandler for ChatFolderOperation { async fn import_from_bytes( &self, - uid: i64, - view_id: &Uuid, - name: &str, - import_type: ImportType, - bytes: Vec, + _uid: i64, + _view_id: &Uuid, + _name: &str, + _import_type: ImportType, + _bytes: Vec, ) -> Result, FlowyError> { Err(FlowyError::not_support()) } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs index d57eea2ae4..d98e32f67d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use bytes::Bytes; use collab::entity::EncodedCollab; use collab_entity::CollabType; @@ -68,7 +69,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { .await?; let row_metas = self .0 - .get_database_row_metas_with_view_id(&view_id, row_oids.clone()) + .get_database_row_metas_with_view_id(view_id, row_oids.clone()) .await?; let row_document_ids = row_metas .iter() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs index bb00d8c6bd..a843a8eb1f 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs @@ -117,9 +117,9 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn create_default_view( &self, user_id: i64, - parent_view_id: &Uuid, + _parent_view_id: &Uuid, view_id: &Uuid, - name: &str, + _name: &str, layout: ViewLayout, ) -> Result<(), FlowyError> { debug_assert_eq!(layout, ViewLayout::Document); @@ -139,8 +139,8 @@ impl FolderOperationHandler for DocumentFolderOperation { &self, uid: i64, view_id: &Uuid, - name: &str, - import_type: ImportType, + _name: &str, + _import_type: ImportType, bytes: Vec, ) -> Result, FlowyError> { let data = DocumentDataPB::try_from(Bytes::from(bytes))?; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs index 9c064acbe3..02b26e71b6 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs @@ -196,7 +196,7 @@ impl FolderQueryService for FolderServiceImpl { } }) .collect::>(); - children.push(parent_view_id.clone()); + children.push(*parent_view_id); children }, _ => vec![], diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 076b77e1d0..b2b2b0a676 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -566,8 +566,8 @@ impl DatabaseManager { ) -> FlowyResult<()> { let database = self.get_database_editor_with_view_id(view_id).await?; let mut summary_row_content = SummaryRowContent::new(); - if let Some(row) = database.get_row(&view_id, &row_id).await { - let fields = database.get_fields(&view_id, None).await; + if let Some(row) = database.get_row(view_id, &row_id).await { + let fields = database.get_fields(view_id, None).await; for field in fields { // When summarizing a row, skip the content in the "AI summary" cell; it does not need to // be summarized. @@ -600,7 +600,7 @@ impl DatabaseManager { // Update the cell with the response from the cloud service. database - .update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response)) + .update_cell_with_changeset(view_id, &row_id, &field_id, BoxAny::new(response)) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index ace2ec52fc..d079bdc8c2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -133,7 +133,7 @@ impl DatabaseEditor { database.clone(), )?; let this = Arc::new(Self { - database_id: database_id.clone(), + database_id, user, database, cell_cache, diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9e78b02f9b..9225b89c03 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -158,7 +158,7 @@ impl DocumentManager { let cloud_service = self.cloud_service.clone(); let cloned_encoded_collab = encoded_collab.clone(); let workspace_id = self.user_service.workspace_id()?; - let doc_id = doc_id.clone(); + let doc_id = *doc_id; tokio::spawn(async move { let _ = cloud_service .create_document_collab(&workspace_id, &doc_id, cloned_encoded_collab) @@ -260,7 +260,7 @@ impl DocumentManager { subscribe_document_snapshot_state(&lock); subscribe_document_sync_state(&lock); } - self.documents.insert(doc_id.clone(), document.clone()); + self.documents.insert(*doc_id, document.clone()); } Ok(document) }, @@ -322,7 +322,7 @@ impl DocumentManager { lock.clean_awareness_local_state(); } - let clone_doc_id = doc_id.clone(); + let clone_doc_id = doc_id; trace!("move document to removing_documents: {}", doc_id); self.removing_documents.insert(doc_id, document); diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index a7ae759709..231bb3852e 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -1,7 +1,6 @@ use std::ops::Deref; use std::sync::{Arc, OnceLock}; -use anyhow::Error; use collab::entity::EncodedCollab; use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; @@ -39,7 +38,7 @@ impl DocumentTest { let builder = Arc::new(AppFlowyCollabBuilder::new( DefaultCollabStorageProvider(), WorkspaceCollabIntegrateImpl { - workspace_id: user.workspace_id.clone(), + workspace_id: user.workspace_id, }, )); @@ -89,7 +88,7 @@ impl DocumentUserService for FakeUser { } fn workspace_id(&self) -> Result { - Ok(self.workspace_id.clone()) + Ok(self.workspace_id) } fn collab_db(&self, _uid: i64) -> Result, FlowyError> { @@ -145,7 +144,7 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_doc_state( &self, document_id: &Uuid, - workspace_id: &Uuid, + _workspace_id: &Uuid, ) -> Result, FlowyError> { let document_id = document_id.to_string(); Err(FlowyError::new( @@ -156,26 +155,26 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_snapshots( &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, + _document_id: &Uuid, + _limit: usize, + _workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } async fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, + _document_id: &Uuid, + _workspace_id: &Uuid, ) -> Result, FlowyError> { Ok(None) } async fn create_document_collab( &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, + _workspace_id: &Uuid, + _document_id: &Uuid, + _encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } @@ -260,7 +259,7 @@ struct WorkspaceCollabIntegrateImpl { } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { fn workspace_id(&self) -> Result { - Ok(self.workspace_id.clone()) + Ok(self.workspace_id) } fn device_id(&self) -> Result { diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 5e29702d53..4f2304846b 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -348,12 +348,12 @@ impl TryInto for CreateViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; let parent_view_id = ViewIdentify::parse(self.parent_view_id) - .and_then(|id| Uuid::from_str(&id.0).map_err(|err| ErrorCode::InvalidParams))?; + .and_then(|id| Uuid::from_str(&id.0).map_err(|_| ErrorCode::InvalidParams))?; // if view_id is not provided, generate a new view_id let view_id = self .view_id .and_then(|v| Uuid::parse_str(&v).ok()) - .unwrap_or_else(|| gen_view_id()); + .unwrap_or_else(gen_view_id); Ok(CreateViewParams { parent_view_id, @@ -379,7 +379,7 @@ impl TryInto for CreateOrphanViewPayloadPB { let view_id = Uuid::parse_str(&self.view_id).map_err(|_| ErrorCode::InvalidParams)?; Ok(CreateViewParams { - parent_view_id: view_id.clone(), + parent_view_id: view_id, name, layout: self.layout, view_id, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 51edf3a11c..180a5dc1f3 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -189,7 +189,7 @@ impl FolderManager { let config = CollabBuilderConfig::default().sync_enable(true); let data_source = data_source.unwrap_or_else(|| { - CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.clone()).into_data_source() + CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source() }); let object_id = workspace_id; @@ -245,7 +245,7 @@ impl FolderManager { .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.clone()).into_data_source(); + CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source(); let folder = self .collab_builder .create_folder( @@ -1221,7 +1221,7 @@ impl FolderManager { } let workspace_id = self.user.workspace_id()?; - let parent_view_id = Uuid::from_str(&parent_view_id)?; + let parent_view_id = Uuid::from_str(parent_view_id)?; // Sync the view to the cloud if sync_after_create { @@ -1815,7 +1815,7 @@ impl FolderManager { for data in import_data.items { // Import a single file and get the view and encoded collab data let (view, encoded_collabs) = self - .import_single_file(import_data.parent_view_id.clone(), data) + .import_single_file(import_data.parent_view_id, data) .await?; views.push(view_pb_without_child_views(view)); @@ -2054,7 +2054,7 @@ impl FolderManager { pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { self .folder_indexer - .remove_indices_for_workspace(workspace_id.clone())?; + .remove_indices_for_workspace(*workspace_id)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index c319d2ef77..d61de1f237 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -74,13 +74,13 @@ impl FolderManager { // This will happen user can't fetch the folder data when the user sign in. let doc_state = self .cloud_service - .get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id) + .get_folder_doc_state(workspace_id, uid, CollabType::Folder, workspace_id) .await?; self .make_folder( uid, - &workspace_id, + workspace_id, collab_db.clone(), Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), @@ -92,14 +92,14 @@ impl FolderManager { if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self - .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) + .make_folder(uid, workspace_id, collab_db, None, folder_notifier) .await? } else { event!(Level::INFO, "Restore folder from remote data"); self .make_folder( uid, - &workspace_id, + workspace_id, collab_db.clone(), Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), @@ -115,27 +115,23 @@ impl FolderManager { let index_content_rx = folder.subscribe_index_content(); self .folder_indexer - .set_index_content_receiver(index_content_rx, workspace_id.clone()); - self.handle_index_folder(workspace_id.clone(), &folder); + .set_index_content_receiver(index_content_rx, *workspace_id); + self.handle_index_folder(*workspace_id, &folder); folder_state_rx }; self.mutex_folder.store(Some(folder.clone())); let weak_mutex_folder = Arc::downgrade(&folder); - subscribe_folder_sync_state_changed( - workspace_id.clone(), - folder_state_rx, - Arc::downgrade(&self.user), - ); + subscribe_folder_sync_state_changed(*workspace_id, folder_state_rx, Arc::downgrade(&self.user)); subscribe_folder_trash_changed( - workspace_id.clone(), + *workspace_id, section_change_rx, weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); subscribe_folder_view_changed( - workspace_id.clone(), + *workspace_id, view_rx, weak_mutex_folder.clone(), Arc::downgrade(&self.user), @@ -198,7 +194,7 @@ impl FolderManager { // We spawn a blocking task to index all views in the folder spawn_blocking(move || { // We remove old indexes just in case - let _ = folder_indexer.remove_indices_for_workspace(workspace_id.clone()); + let _ = folder_indexer.remove_indices_for_workspace(workspace_id); // We index all views from the workspace folder_indexer.index_all_views(views, workspace_id); diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index a6faede8b5..5d3034b5aa 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -99,7 +99,7 @@ pub(crate) fn subscribe_folder_sync_state_changed( } folder_notification_builder( - &workspace_id.to_string(), + workspace_id.to_string(), FolderNotification::DidUpdateFolderSyncUpdate, ) .payload(FolderSyncStatePB::from(state)) diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 509abcb955..17989aca29 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -295,7 +295,7 @@ impl IndexManager for FolderIndexManagerImpl { fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { let indexer = self.clone(); - let wid = workspace_id.clone(); + let wid = workspace_id; tokio::spawn(async move { while let Ok(msg) = rx.recv().await { match msg { @@ -306,7 +306,7 @@ impl IndexManager for FolderIndexManagerImpl { data: view.name, icon: view.icon, layout: view.layout, - workspace_id: wid.clone(), + workspace_id: wid, }); }, Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), @@ -318,7 +318,7 @@ impl IndexManager for FolderIndexManagerImpl { data: view.name, icon: view.icon, layout: view.layout, - workspace_id: wid.clone(), + workspace_id: wid, }); }, Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), @@ -424,7 +424,7 @@ impl FolderIndexManager for FolderIndexManagerImpl { fn index_all_views(&self, views: Vec>, workspace_id: Uuid) { let indexable_data = views .into_iter() - .map(|view| IndexableData::from_view(view, workspace_id.clone())) + .map(|view| IndexableData::from_view(view, workspace_id)) .collect(); let _ = self.index_all(indexable_data); @@ -442,14 +442,14 @@ impl FolderIndexManager for FolderIndexManagerImpl { FolderViewChange::Inserted { view_id } => { let view = views_iter.find(|view| view.id == view_id); if let Some(view) = view { - let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let indexable_data = IndexableData::from_view(view, workspace_id); let _ = self.add_index(indexable_data); } }, FolderViewChange::Updated { view_id } => { let view = views_iter.find(|view| view.id == view_id); if let Some(view) = view { - let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let indexable_data = IndexableData::from_view(view, workspace_id); let _ = self.update_index(indexable_data); } }, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 6f5edc93cc..24798d768d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ ChatQuestionQuery, CompleteTextParams, RepeatedRelatedQuestion, ResponseFormat, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index f862c57fc4..c493dda344 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; @@ -41,8 +42,8 @@ where let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id.clone(), collab_type), + workspace_id: *workspace_id, + inner: QueryCollab::new(*object_id, collab_type), }; let result = try_get_client?.get_collab(params).await; match result { @@ -77,8 +78,8 @@ where .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?; let params = CreateCollabParams { - workspace_id: workspace_id.clone(), - object_id: object_id.clone(), + workspace_id: *workspace_id, + object_id: *object_id, encoded_collab_v1, collab_type, }; @@ -151,7 +152,7 @@ where .map(|(key, value)| (key, Value::String(value))) .collect(); let params = SummarizeRowParams { - workspace_id: workspace_id.clone(), + workspace_id: *workspace_id, data: SummarizeRowData::Content(map), }; let data = try_get_client?.summarize_row(params).await?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index e5e271b1e5..4909d96fef 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -33,8 +34,8 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(document_id.clone(), CollabType::Document), + workspace_id: *workspace_id, + inner: QueryCollab::new(*document_id, CollabType::Document), }; let doc_state = self .inner @@ -71,8 +72,8 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(document_id.clone(), CollabType::Document), + workspace_id: *workspace_id, + inner: QueryCollab::new(*document_id, CollabType::Document), }; let doc_state = self .inner @@ -105,8 +106,8 @@ where encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let params = CreateCollabParams { - workspace_id: workspace_id.clone(), - object_id: document_id.clone(), + workspace_id: *workspace_id, + object_id: *document_id, encoded_collab_v1: encoded_collab .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 3a912306fc..5b8efa4b32 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use tracing::{instrument, trace}; use uuid::Uuid; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, FullSyncCollabParams, Workspace, WorkspaceRecord, @@ -93,8 +93,8 @@ where let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder), + workspace_id: *workspace_id, + inner: QueryCollab::new(*workspace_id, CollabType::Folder), }; let doc_state = try_get_client? .get_collab(params) @@ -103,7 +103,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder data")?; + check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder data")?; let folder = Folder::from_collab_doc_state( uid, CollabOrigin::Empty, @@ -126,15 +126,15 @@ where async fn get_folder_doc_state( &self, workspace_id: &Uuid, - uid: i64, + _uid: i64, collab_type: CollabType, object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id.clone(), collab_type), + workspace_id: *workspace_id, + inner: QueryCollab::new(*object_id, collab_type), }; let doc_state = try_get_client? .get_collab(params) @@ -143,7 +143,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder doc state")?; + check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder doc state")?; Ok(doc_state) } @@ -155,7 +155,7 @@ where let try_get_client = self.inner.try_get_client(); try_get_client? .collab_full_sync( - &workspace_id, + workspace_id, ¶ms.object_id, params.collab_type, params.encoded_collab.doc_state.to_vec(), @@ -220,7 +220,7 @@ where }) .collect::>(); try_get_client? - .publish_collabs(&workspace_id, params) + .publish_collabs(workspace_id, params) .await?; Ok(()) } @@ -232,7 +232,7 @@ where ) -> Result<(), FlowyError> { let try_get_client = self.inner.try_get_client(); try_get_client? - .unpublish_collabs(&workspace_id, &view_ids) + .unpublish_collabs(workspace_id, &view_ids) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 45ba817bce..6b2917cc62 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -193,7 +193,7 @@ where async fn open_workspace(&self, workspace_id: &Uuid) -> Result { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let af_workspace = client.open_workspace(&workspace_id).await?; + let af_workspace = client.open_workspace(workspace_id).await?; Ok(to_user_workspace(af_workspace)) } @@ -348,18 +348,18 @@ where #[instrument(level = "debug", skip_all)] async fn get_user_awareness_doc_state( &self, - uid: i64, + _uid: i64, workspace_id: &Uuid, object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id.clone(), CollabType::UserAwareness), + workspace_id: *workspace_id, + inner: QueryCollab::new(*object_id, CollabType::UserAwareness), }; let resp = try_get_client?.get_collab(params).await?; - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get user awareness object")?; + check_request_workspace_id_is_match(workspace_id, &cloned_user, "get user awareness object")?; Ok(resp.encode_collab.doc_state.to_vec()) } @@ -403,12 +403,12 @@ where .into_iter() .flat_map(|object| { Uuid::from_str(&object.object_id) - .and_then(|object_id| { - Ok(CollabParams::new( + .map(|object_id| { + CollabParams::new( object_id, u8::from(object.collab_type).into(), object.encoded_collab, - )) + ) }) .ok() }) @@ -456,7 +456,7 @@ where let try_get_client = self.server.try_get_client(); let client = try_get_client?; let params = QueryWorkspaceMember { - workspace_id: workspace_id.clone(), + workspace_id: *workspace_id, uid, }; let member = client.get_workspace_member(params).await?; diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 73633216b3..02e313115f 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -17,104 +17,104 @@ pub(crate) struct DefaultChatCloudServiceImpl; impl ChatCloudService for DefaultChatCloudServiceImpl { async fn create_chat( &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, + _uid: &i64, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _rag_ids: Vec, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn create_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], + _workspace_id: &Uuid, + _chat_id: &Uuid, + _message: &str, + _message_type: ChatMessageType, + _metadata: &[ChatMessageMetadata], ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn create_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _message: &str, + _question_id: i64, + _metadata: Option, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - format: ResponseFormat, - ai_model: Option, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _message_id: i64, + _format: ResponseFormat, + _ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _offset: MessageCursor, + _limit: u64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_question_from_answer_id( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _answer_message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_message_id: i64, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _question_message_id: i64, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn stream_complete( &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option, + _workspace_id: &Uuid, + _params: CompleteTextParams, + _ai_model: Option, ) -> Result { Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) } async fn embed_file( &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option>, + _workspace_id: &Uuid, + _file_path: &Path, + _chat_id: &Uuid, + _metadata: Option>, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { + async fn get_local_ai_config(&self, _workspace_id: &Uuid) -> Result { Err( FlowyError::not_support() .with_context("Get local ai config is not supported in local server."), @@ -123,7 +123,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { async fn get_workspace_plan( &self, - workspace_id: &Uuid, + _workspace_id: &Uuid, ) -> Result, FlowyError> { Err( FlowyError::not_support() @@ -133,26 +133,26 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { async fn get_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + _workspace_id: &Uuid, + _chat_id: &Uuid, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn update_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, + _workspace_id: &Uuid, + _id: &Uuid, + _s: UpdateChatParams, ) -> Result<(), FlowyError> { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } - async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { + async fn get_workspace_default_model(&self, _workspace_id: &Uuid) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 36d31902f0..46b0cdd649 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use collab::entity::EncodedCollab; use collab_database::database::default_database_data; use collab_database::workspace_database::default_workspace_database_data; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index fe44470921..c553026274 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use collab::entity::EncodedCollab; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index a90352a976..7bb3139953 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use std::sync::Arc; use crate::local_server::LocalServerDB; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index ddea5353d8..962c0506bf 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,3 +1,4 @@ +#![allow(unused_variables)] use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; diff --git a/frontend/rust-lib/flowy-user/src/services/billing_check.rs b/frontend/rust-lib/flowy-user/src/services/billing_check.rs index b15f811ab6..ea5bc65b75 100644 --- a/frontend/rust-lib/flowy-user/src/services/billing_check.rs +++ b/frontend/rust-lib/flowy-user/src/services/billing_check.rs @@ -47,7 +47,7 @@ impl PeriodicallyCheckBillingState { while attempts < max_attempts { let plans = cloud_service .get_user_service()? - .get_workspace_plan(self.workspace_id.clone()) + .get_workspace_plan(self.workspace_id) .await?; // If the expected plan is not set, return the plans immediately. Otherwise, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index cc9cddabb5..d055621398 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -231,7 +231,7 @@ impl UserManager { let workspace_id = session.user_workspace.workspace_id()?; let create_awareness = if authenticator.is_local() { let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id.clone()) + CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) .into_data_source(); Self::collab_for_user_awareness( &weak_builder, @@ -266,12 +266,9 @@ impl UserManager { Err(err) => { if err.is_record_not_found() { info!("User awareness not found, creating new"); - let doc_state = CollabPersistenceImpl::new( - collab_db.clone(), - session.user_id, - workspace_id.clone(), - ) - .into_data_source(); + let doc_state = + CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) + .into_data_source(); Self::collab_for_user_awareness( &weak_builder, &workspace_id, From 24d57336a979829ba6850f0ddf7a9a125bd98c3d Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 7 Apr 2025 21:15:30 +0800 Subject: [PATCH 274/384] chore: bump collab id --- frontend/rust-lib/Cargo.lock | 16 ++++----- frontend/rust-lib/Cargo.toml | 16 ++++----- .../collab-integrate/src/collab_builder.rs | 13 ++++--- .../tests/folder/local_test/folder_test.rs | 8 ++--- .../local_test/publish_document_test.rs | 25 ++++++------- .../tests/user/migration_test/version_test.rs | 35 ------------------- .../rust-lib/flowy-document/src/manager.rs | 8 ++++- 7 files changed, 46 insertions(+), 75 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e10038c7d3..baa5c5ee6d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "arc-swap", @@ -1273,7 +1273,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "async-trait", @@ -1313,7 +1313,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "arc-swap", @@ -1334,7 +1334,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "bytes", @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "arc-swap", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "async-recursion", @@ -1439,7 +1439,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "async-stream", @@ -1517,7 +1517,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4a0e2cc07f50f17d1b6605c579e622a431e94998#4a0e2cc07f50f17d1b6605c579e622a431e94998" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index e3cb5a4178..78f67f8f90 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4a0e2cc07f50f17d1b6605c579e622a431e94998" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 226a5b679b..e31c38d043 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -1,6 +1,5 @@ use std::borrow::BorrowMut; use std::fmt::{Debug, Display}; -use std::str::FromStr; use std::sync::{Arc, Weak}; use crate::CollabKVDB; @@ -138,7 +137,6 @@ impl AppFlowyCollabBuilder { )); } let device_id = self.workspace_integrate.device_id()?; - let workspace_id = self.workspace_integrate.workspace_id()?; Ok(CollabObject::new( uid, object_id.to_string(), @@ -426,13 +424,13 @@ impl CollabPersistence for CollabPersistenceImpl { .upgrade() .ok_or_else(|| CollabError::Internal(anyhow!("collab_db is dropped")))?; - let object_id = - Uuid::from_str(collab.object_id()).map_err(|v| CollabError::Internal(v.into()))?; + let object_id = collab.object_id().to_string(); let rocksdb_read = collab_db.read_txn(); + let workspace_id = self.workspace_id.to_string(); - if rocksdb_read.is_exist(self.uid, &self.workspace_id, &object_id) { + if rocksdb_read.is_exist(self.uid, &workspace_id, &object_id) { let mut txn = collab.transact_mut(); - match rocksdb_read.load_doc_with_txn(self.uid, &self.workspace_id, &object_id, &mut txn) { + match rocksdb_read.load_doc_with_txn(self.uid, &workspace_id, &object_id, &mut txn) { Ok(update_count) => { trace!( "did load collab:{}-{} from disk, update_count:{}", @@ -457,6 +455,7 @@ impl CollabPersistence for CollabPersistenceImpl { object_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), CollabError> { + let workspace_id = self.workspace_id.to_string(); let collab_db = self .db .upgrade() @@ -465,7 +464,7 @@ impl CollabPersistence for CollabPersistenceImpl { write_txn .flush_doc( self.uid, - self.workspace_id.to_string().as_str(), + workspace_id.as_str(), object_id, encoded_collab.state_vector.to_vec(), encoded_collab.doc_state.to_vec(), diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs index d0a4a28429..5857190b8b 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs @@ -1,8 +1,8 @@ use collab_folder::ViewLayout; - use event_integration_test::EventIntegrationTest; use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB}; use flowy_folder::entities::ViewLayoutPB; +use uuid::Uuid; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; @@ -338,11 +338,11 @@ async fn move_view_event_test() { async fn create_orphan_child_view_and_get_its_ancestors_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = "20240521"; + let view_id = Uuid::new_v4().to_string(); test - .create_orphan_view(name, view_id, ViewLayoutPB::Grid) + .create_orphan_view(name, &view_id, ViewLayoutPB::Grid) .await; - let ancestors = test.get_view_ancestors(view_id).await; + let ancestors = test.get_view_ancestors(&view_id).await; assert_eq!(ancestors.len(), 1); assert_eq!(ancestors[0].name, "Orphan View"); assert_eq!(ancestors[0].id, view_id); diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs index 0d5b9bc08c..089310b260 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs @@ -6,6 +6,7 @@ use flowy_folder::view_operation::GatherEncodedCollab; use flowy_folder_pub::entities::{ PublishDocumentPayload, PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData, }; +use uuid::Uuid; async fn mock_single_document_view_publish_payload( test: &EventIntegrationTest, @@ -140,11 +141,11 @@ async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name #[tokio::test] async fn single_document_get_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; - let view_id = "20240521"; + let view_id = Uuid::new_v4().to_string(); let name = "Orphan View"; - create_single_document(&test, view_id, name).await; - let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, true).await; + create_single_document(&test, &view_id, name).await; + let view = test.get_view(&view_id).await; + let payload = test.get_publish_payload(&view_id, true).await; let expect_payload = mock_single_document_view_publish_payload( &test, @@ -160,10 +161,10 @@ async fn single_document_get_publish_view_payload_test() { async fn nested_document_get_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = "20240521"; - create_nested_document(&test, view_id, name).await; - let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, true).await; + let view_id = Uuid::new_v4().to_string(); + create_nested_document(&test, &view_id, name).await; + let view = test.get_view(&view_id).await; + let payload = test.get_publish_payload(&view_id, true).await; let expect_payload = mock_nested_document_view_publish_payload( &test, @@ -180,10 +181,10 @@ async fn nested_document_get_publish_view_payload_test() { async fn no_children_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = "20240521"; - create_nested_document(&test, view_id, name).await; - let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, false).await; + let view_id = Uuid::new_v4().to_string(); + create_nested_document(&test, &view_id, name).await; + let view = test.get_view(&view_id).await; + let payload = test.get_publish_payload(&view_id, false).await; let data = mock_single_document_view_publish_payload( &test, diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs index 5a75197a9c..61833429aa 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs @@ -1,44 +1,9 @@ use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_folder::entities::ViewLayoutPB; use std::time::Duration; use crate::util::unzip; -#[tokio::test] -async fn migrate_020_historical_empty_document_test() { - let user_db_path = unzip( - "./tests/user/migration_test/history_user_db", - "020_historical_user_data", - ) - .unwrap(); - let test = - EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; - - let mut views = test.get_all_workspace_views().await; - assert_eq!(views.len(), 1); - - // Check the parent view - let parent_view = views.pop().unwrap(); - assert_eq!(parent_view.layout, ViewLayoutPB::Document); - let data = test.open_document(parent_view.id.clone()).await.data; - assert!(!data.page_id.is_empty()); - assert_eq!(data.blocks.len(), 2); - assert!(!data.meta.children_map.is_empty()); - - // Check the child views of the parent view - let child_views = test.get_view(&parent_view.id).await.child_views; - assert_eq!(child_views.len(), 4); - assert_eq!(child_views[0].layout, ViewLayoutPB::Document); - assert_eq!(child_views[1].layout, ViewLayoutPB::Grid); - assert_eq!(child_views[2].layout, ViewLayoutPB::Calendar); - assert_eq!(child_views[3].layout, ViewLayoutPB::Board); - - let database = test.get_database(&child_views[1].id).await; - assert_eq!(database.fields.len(), 8); - assert_eq!(database.rows.len(), 3); -} - #[tokio::test] async fn migrate_036_fav_v1_workspace_array_test() { // Used to test migration: FavoriteV1AndWorkspaceArrayMigration diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9225b89c03..704dcd0865 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -482,7 +482,13 @@ async fn doc_state_from_document_data( data: Option, ) -> Result { let doc_id = doc_id.to_string(); - let data = data.unwrap_or_else(|| default_document_data(&doc_id)); + let data = data.unwrap_or_else(|| { + trace!( + "{} document data is None, use default document data", + doc_id.to_string() + ); + default_document_data(&doc_id) + }); // spawn_blocking is used to avoid blocking the tokio thread pool if the document is large. let encoded_collab = tokio::task::spawn_blocking(move || { let collab = Collab::new_with_origin(CollabOrigin::Empty, doc_id, vec![], false); From 20bcdd1f904e67cf322521a10ae14ad2edb9325e Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 7 Apr 2025 21:28:48 +0800 Subject: [PATCH 275/384] chore: fmt --- .../rust-lib/flowy-core/src/deps_resolve/document_deps.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index 533ac6f931..3527bc42d6 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -1,5 +1,3 @@ -use std::sync::{Arc, Weak}; -use uuid::Uuid; use crate::deps_resolve::CollabSnapshotSql; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; @@ -10,6 +8,8 @@ use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{FlowyError, FlowyResult}; use flowy_storage_pub::storage::StorageService; use flowy_user::services::authenticate_user::AuthenticateUser; +use std::sync::{Arc, Weak}; +use uuid::Uuid; pub struct DocumentDepsResolver(); impl DocumentDepsResolver { From 4896d7c1bec9799abb8ec4f333d2f9693dd399a9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 7 Apr 2025 22:04:32 +0800 Subject: [PATCH 276/384] chore: enable sync log by default --- .../settings/widgets/setting_appflowy_cloud.dart | 2 +- frontend/rust-lib/flowy-core/src/config.rs | 10 ++++------ frontend/rust-lib/flowy-document/src/event_handler.rs | 10 ++++------ .../flowy-server-pub/src/native/af_cloud_config.rs | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 645c3daa65..a9cb362bf3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -130,7 +130,7 @@ class CustomAppFlowyCloudView extends StatelessWidget { final List children = []; children.addAll([ const AppFlowyCloudEnableSync(), - const AppFlowyCloudSyncLogEnabled(), + // const AppFlowyCloudSyncLogEnabled(), const VSpace(40), ]); diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 2b379ab63a..33386249e6 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -84,14 +84,12 @@ impl AppFlowyCoreConfig { ) -> Self { let cloud_config = AFCloudConfiguration::from_env().ok(); let mut log_crates = vec![]; + // By default enable sync trace log + log_crates.push("sync_trace_log".to_string()); + let storage_path = match &cloud_config { None => custom_application_path, - Some(config) => { - if config.enable_sync_trace { - log_crates.push("sync_trace_log".to_string()); - } - make_user_data_folder(&custom_application_path, &config.base_url) - }, + Some(config) => make_user_data_folder(&custom_application_path, &config.base_url), }; let log_filter = create_log_filter( diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index cb16129c40..acf45777eb 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -21,6 +21,7 @@ use crate::parser::parser_entities::{ use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_infra::sync_trace; use tracing::instrument; use uuid::Uuid; @@ -124,9 +125,7 @@ pub(crate) async fn apply_action_handler( let doc_id = params.document_id; let document = manager.editable_document(&doc_id).await?; let actions = params.actions; - if cfg!(feature = "verbose_log") { - tracing::trace!("{} applying actions: {:?}", doc_id, actions); - } + sync_trace!("{} applying action: {:?}", doc_id, actions); document.write().await.apply_action(actions)?; Ok(()) } @@ -141,6 +140,7 @@ pub(crate) async fn create_text_handler( let doc_id = params.document_id; let document = manager.editable_document(&doc_id).await?; let mut document = document.write().await; + sync_trace!("{} creating text: {:?}", doc_id, params.delta); document.apply_text_delta(¶ms.text_id, params.delta); Ok(()) } @@ -157,9 +157,7 @@ pub(crate) async fn apply_text_delta_handler( let text_id = params.text_id; let delta = params.delta; let mut document = document.write().await; - if cfg!(feature = "verbose_log") { - tracing::trace!("{} applying delta: {:?}", doc_id, delta); - } + sync_trace!("{} applying delta: {:?}", doc_id, delta); document.apply_text_delta(&text_id, delta); Ok(()) } diff --git a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs index 14a72c6ce6..9c74850fcd 100644 --- a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs +++ b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs @@ -60,7 +60,7 @@ impl AFCloudConfiguration { let enable_sync_trace = std::env::var(APPFLOWY_ENABLE_SYNC_TRACE) .map(|v| v == "true" || v == "1") - .unwrap_or(false); + .unwrap_or(true); Ok(Self { base_url, From 0286678286fc4fa05118bee63ebd1254b5f42106 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 8 Apr 2025 10:30:48 +0800 Subject: [PATCH 277/384] chore: clippy --- frontend/rust-lib/flowy-core/src/config.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 33386249e6..0067eff5a1 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -83,10 +83,8 @@ impl AppFlowyCoreConfig { name: String, ) -> Self { let cloud_config = AFCloudConfiguration::from_env().ok(); - let mut log_crates = vec![]; // By default enable sync trace log - log_crates.push("sync_trace_log".to_string()); - + let log_crates = vec!["sync_trace_log".to_string()]; let storage_path = match &cloud_config { None => custom_application_path, Some(config) => make_user_data_folder(&custom_application_path, &config.base_url), From 9e30b1816fe613a3d6e6051c5b016f30f1e2a2b8 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:46:06 +0800 Subject: [PATCH 278/384] fix(flutter_desktop): grid bug (#7697) * fix: field width doesn't persist * test: add test * test: fix test * test: grid width integration test --- .../database_field_settings_test.dart | 41 +++++++++++++++++++ .../shared/database_test_op.dart | 25 +++++++++++ .../application/field/field_controller.dart | 17 +++++--- .../widgets/header/desktop_field_cell.dart | 9 ++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart index 9b9434d3d7..a71110f1e0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart @@ -15,6 +15,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); + // create a database and add a linked database view await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); @@ -29,6 +30,11 @@ void main() { await tester.tapHidePropertyButton(); tester.noFieldWithName('New field 1'); + // create another field, New field 1 to be hidden still + await tester.tapNewPropertyButton(); + await tester.dismissFieldEditor(); + tester.noFieldWithName('New field 1'); + // go back to inline database view, expect field to be shown await tester.tapTabBarLinkedViewByViewName('Untitled'); tester.findFieldWithName('New field 1'); @@ -60,5 +66,40 @@ void main() { await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1"); }); + + testWidgets('field cell width', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + // create a database and add a linked database view + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); + + // create a field + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); + + // check the width of the field + expect(tester.getFieldWidth('New field 1'), 150); + + // change the width of the field + await tester.changeFieldWidth('New field 1', 200); + expect(tester.getFieldWidth('New field 1'), 205); + + // create another field, New field 1 to be same width + await tester.tapNewPropertyButton(); + await tester.dismissFieldEditor(); + expect(tester.getFieldWidth('New field 1'), 205); + + // go back to inline database view, expect New field 1 to be 150px + await tester.tapTabBarLinkedViewByViewName('Untitled'); + expect(tester.getFieldWidth('New field 1'), 150); + + // go back to linked database view, expect New field 1 to be 205px + await tester.tapTabBarLinkedViewByViewName('Grid'); + expect(tester.getFieldWidth('New field 1'), 205); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 9a82c881e0..970965f294 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -942,6 +942,31 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(const Duration(milliseconds: 200)); } + Future changeFieldWidth(String fieldName, double width) async { + final field = find.byWidgetPredicate( + (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName, + ); + await hoverOnWidget( + field, + onHover: () async { + final dragHandle = find.descendant( + of: field, + matching: find.byType(DragToExpandLine), + ); + await drag(dragHandle, Offset(width - getSize(field).width, 0)); + await pumpAndSettle(const Duration(milliseconds: 200)); + }, + ); + } + + double getFieldWidth(String fieldName) { + final field = find.byWidgetPredicate( + (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName, + ); + + return getSize(field).width; + } + Future findDateEditor(dynamic matcher) async { final finder = find.byType(DateCellEditor); expect(finder, matcher); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index 8370bd9bff..93fd69bcfc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -411,23 +411,28 @@ class FieldController { /// Listen for field setting changes in the backend. void _listenOnFieldSettingsChanged() { FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) { - final List newFields = fieldInfos; - var updatedField = newFields.firstOrNull; + final newFields = [...fieldInfos]; - if (updatedField == null) { + if (newFields.isEmpty) { return null; } final index = newFields .indexWhere((field) => field.id == updatedFieldSettings.fieldId); + if (index != -1) { newFields[index] = newFields[index].copyWith(fieldSettings: updatedFieldSettings); - updatedField = newFields[index]; + _fieldNotifier.fieldInfos = newFields; + _fieldSettings + ..removeWhere( + (field) => field.fieldId == updatedFieldSettings.fieldId, + ) + ..add(updatedFieldSettings); + return newFields[index]; } - _fieldNotifier.fieldInfos = newFields; - return updatedField; + return null; } _fieldSettingsListener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index 23c2fe1f91..915bf70a61 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -108,7 +108,7 @@ class _GridFieldCellState extends State { top: 0, bottom: 0, right: 0, - child: _DragToExpandLine(), + child: DragToExpandLine(), ); return _GridHeaderCellContainer( @@ -158,8 +158,11 @@ class _GridHeaderCellContainer extends StatelessWidget { } } -class _DragToExpandLine extends StatelessWidget { - const _DragToExpandLine(); +@visibleForTesting +class DragToExpandLine extends StatelessWidget { + const DragToExpandLine({ + super.key, + }); @override Widget build(BuildContext context) { From 462c822255b91f276dfd122d6b8752b41c807aa4 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:08:10 +0800 Subject: [PATCH 279/384] fix: translations (#7694) --- .../presentation/base/view_page/more_bottom_sheet.dart | 2 +- .../presentation/message/ai_message_action_bar.dart | 2 +- .../ai_chat/presentation/message/ai_message_bubble.dart | 2 +- .../database/widgets/cell/editable_cell_skeleton/url.dart | 2 +- .../lib/plugins/shared/share/export_tab.dart | 2 +- .../lib/plugins/shared/share/publish_tab.dart | 2 +- .../lib/plugins/shared/share/share_tab.dart | 2 +- .../presentation/settings/pages/sites/constants.dart | 2 +- frontend/resources/translations/ar-SA.json | 3 +-- frontend/resources/translations/de-DE.json | 3 +-- frontend/resources/translations/el-GR.json | 5 ++--- frontend/resources/translations/en.json | 7 +++---- frontend/resources/translations/es-VE.json | 3 +-- frontend/resources/translations/fr-FR.json | 3 +-- frontend/resources/translations/he.json | 3 +-- frontend/resources/translations/ja-JP.json | 3 +-- frontend/resources/translations/ko-KR.json | 3 +-- frontend/resources/translations/ru-RU.json | 3 +-- frontend/resources/translations/th-TH.json | 3 +-- frontend/resources/translations/tr-TR.json | 3 +-- frontend/resources/translations/uk-UA.json | 3 +-- frontend/resources/translations/vi-VN.json | 3 +-- frontend/resources/translations/zh-CN.json | 3 +-- frontend/resources/translations/zh-TW.json | 3 +-- 24 files changed, 27 insertions(+), 43 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index dd659420d6..080d83e83c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -203,7 +203,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ); showToastNotification( context, - message: LocaleKeys.grid_url_copy.tr(), + message: LocaleKeys.message_copy_success.tr(), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 150ce20192..945613490a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -185,7 +185,7 @@ class CopyButton extends StatelessWidget { if (context.mounted) { showToastNotification( context, - message: LocaleKeys.grid_url_copiedNotification.tr(), + message: LocaleKeys.message_copy_success.tr(), ); } }, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index eed5f0a520..3ce80e2919 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -377,7 +377,7 @@ class ChatAIMessagePopup extends StatelessWidget { if (context.mounted) { showToastNotification( context, - message: LocaleKeys.grid_url_copiedNotification.tr(), + message: LocaleKeys.message_copy_success.tr(), ); } }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart index 0dc7779e55..39616dbcf8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart @@ -203,7 +203,7 @@ class MobileURLEditor extends StatelessWidget { ClipboardData(text: textEditingController.text), ); Fluttertoast.showToast( - msg: LocaleKeys.grid_url_copiedNotification.tr(), + msg: LocaleKeys.message_copy_success.tr(), gravity: ToastGravity.BOTTOM, ); context.pop(); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index fc2a4507bd..e4a23b6459 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -175,7 +175,7 @@ class ExportTab extends StatelessWidget { ); showToastNotification( context, - message: LocaleKeys.grid_url_copiedNotification.tr(), + message: LocaleKeys.message_copy_success.tr(), ); }, (error) => showToastNotification(context, message: error.msg), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 1f754a4372..0ffa22a43b 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -183,7 +183,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> { showToastNotification( context, - message: LocaleKeys.grid_url_copy.tr(), + message: LocaleKeys.message_copy_success.tr(), ); }, onSubmitted: (pathName) { diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart index 6980edce46..1b925cfec6 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart @@ -118,7 +118,7 @@ class _ShareTabContent extends StatelessWidget { showToastNotification( context, - message: LocaleKeys.grid_url_copy.tr(), + message: LocaleKeys.message_copy_success.tr(), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart index f764bec9e7..1a3b305c0b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart @@ -54,7 +54,7 @@ class SettingsPageSitesEvent { getIt().setData(ClipboardServiceData(plainText: url)); showToastNotification( context, - message: LocaleKeys.grid_url_copy.tr(), + message: LocaleKeys.message_copy_success.tr(), ); } } diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index cc5d11719d..4afd35c40e 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -1672,8 +1672,7 @@ "url": { "launch": "فتح في المتصفح", "copy": "إنسخ الرابط", - "textFieldHint": "أدخل عنوان URL", - "copiedNotification": "تمت نسخها إلى الحافظة!" + "textFieldHint": "أدخل عنوان URL" }, "relation": { "relatedDatabasePlaceLabel": "قاعدة البيانات ذات الصلة", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index e95b40d26d..60bb99b2fb 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -1625,8 +1625,7 @@ "url": { "launch": "Im Browser öffnen", "copy": "Webadresse kopieren", - "textFieldHint": "Gebe eine URL ein", - "copiedNotification": "In die Zwischenablage kopiert!" + "textFieldHint": "Gebe eine URL ein" }, "relation": { "relatedDatabasePlaceLabel": "Verwandte Datenbank", diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json index 6d6524a8bf..a329a8998c 100644 --- a/frontend/resources/translations/el-GR.json +++ b/frontend/resources/translations/el-GR.json @@ -723,9 +723,8 @@ }, "url": { "launch": "Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης", - "copy": "Copied link to clipboard", - "textFieldHint": "Enter a URL", - "copiedNotification": "Copied to clipboard!" + "copy": "Copy link to clipboard", + "textFieldHint": "Enter a URL" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 27b98fdce5..47a2bec391 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1647,9 +1647,8 @@ }, "url": { "launch": "Open link in browser", - "copy": "Copied link to clipboard", - "textFieldHint": "Enter a URL", - "copiedNotification": "Copied to clipboard!" + "copy": "Copy link to clipboard", + "textFieldHint": "Enter a URL" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", @@ -2273,7 +2272,7 @@ }, "message": { "copy": { - "success": "Copied!", + "success": "Copied to clipboard", "fail": "Unable to copy" } }, diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 31174c3dc5..e73165349a 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -866,8 +866,7 @@ "url": { "launch": "Abrir en el navegador", "copy": "Copiar URL", - "textFieldHint": "Introduce una URL", - "copiedNotification": "¡Copiado al portapapeles!" + "textFieldHint": "Introduce una URL" }, "relation": { "relatedDatabasePlaceLabel": "Base de datos relacionada", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 0d542e916b..f2c3e1270d 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -1612,8 +1612,7 @@ "url": { "launch": "Ouvrir dans le navigateur", "copy": "Copier l'URL", - "textFieldHint": "Entrez une URL", - "copiedNotification": "Copié dans le presse-papier!" + "textFieldHint": "Entrez une URL" }, "relation": { "relatedDatabasePlaceLabel": "Base de données associée", diff --git a/frontend/resources/translations/he.json b/frontend/resources/translations/he.json index d47c33c6da..eda0cd920f 100644 --- a/frontend/resources/translations/he.json +++ b/frontend/resources/translations/he.json @@ -1243,8 +1243,7 @@ "url": { "launch": "פתיחת קישור בדפדפן", "copy": "העתקת קישור ללוח הגזירים", - "textFieldHint": "נא למלא כתובת", - "copiedNotification": "הועתק ללוח הגזירים!" + "textFieldHint": "נא למלא כתובת" }, "relation": { "relatedDatabasePlaceLabel": "מסד נתונים קשור", diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index cd41f16530..15333ff3da 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -1577,8 +1577,7 @@ "url": { "launch": "リンクをブラウザで開く", "copy": "リンクをクリップボードにコピー", - "textFieldHint": "URLを入力", - "copiedNotification": "クリップボードにコピーされました!" + "textFieldHint": "URLを入力" }, "relation": { "relatedDatabasePlaceLabel": "関連データベース", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 66240d954a..0b2aabe508 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -1634,8 +1634,7 @@ "url": { "launch": "브라우저에서 링크 열기", "copy": "링크를 클립보드에 복사", - "textFieldHint": "URL 입력", - "copiedNotification": "클립보드에 복사되었습니다!" + "textFieldHint": "URL 입력" }, "relation": { "relatedDatabasePlaceLabel": "관련 데이터베이스", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 6ca29ffc03..2bac18a879 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -1420,8 +1420,7 @@ "url": { "launch": "Открыть в браузере", "copy": "Скопировать URL", - "textFieldHint": "Введите URL-адрес", - "copiedNotification": "Скопировано в буфер обмена!" + "textFieldHint": "Введите URL-адрес" }, "relation": { "relatedDatabasePlaceLabel": "Связанная база данных", diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 0e888fdb9b..0be97f517e 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -1570,8 +1570,7 @@ "url": { "launch": "เปิดในเบราว์เซอร์", "copy": "คัดลอก URL", - "textFieldHint": "ป้อน URL", - "copiedNotification": "คัดลอกไปยังคลิปบอร์ดแล้ว!" + "textFieldHint": "ป้อน URL" }, "relation": { "relatedDatabasePlaceLabel": "ฐานข้อมูลที่เกี่ยวข้อง", diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 346742f295..0e830b62c4 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1608,8 +1608,7 @@ "url": { "launch": "Bağlantıyı tarayıcıda aç", "copy": "Bağlantıyı panoya kopyala", - "textFieldHint": "Bir URL girin", - "copiedNotification": "Panoya kopyalandı!" + "textFieldHint": "Bir URL girin" }, "relation": { "relatedDatabasePlaceLabel": "İlişkili Veritabanı", diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 8227fc4c53..3262de34a7 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -1445,8 +1445,7 @@ "url": { "launch": "Відкрити посилання в браузері", "copy": "Копіювати посилання в буфер обміну", - "textFieldHint": "Введіть URL", - "copiedNotification": "Скопійовано в буфер обміну!" + "textFieldHint": "Введіть URL" }, "relation": { "relatedDatabasePlaceLabel": "Пов'язана база даних", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 7e27e8a648..957fdd6e03 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -1439,8 +1439,7 @@ "url": { "launch": "Mở liên kết trong trình duyệt", "copy": "Sao chép URL", - "textFieldHint": "Nhập một URL", - "copiedNotification": "Đã sao chép vào bảng tạm!" + "textFieldHint": "Nhập một URL" }, "relation": { "relatedDatabasePlaceLabel": "Cơ sở dữ liệu liên quan", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 557916b5b5..44c46d8b5c 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1270,8 +1270,7 @@ "url": { "launch": "在浏览器中打开链接", "copy": "将链接复制到剪贴板", - "textFieldHint": "输入 URL", - "copiedNotification": "已复制到剪贴板!" + "textFieldHint": "输入 URL" }, "relation": { "rowSearchTextFieldPlaceholder": "搜索" diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index fe66a58aa8..acd121049e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -838,8 +838,7 @@ "url": { "launch": "在瀏覽器中開啟", "copy": "複製網址", - "textFieldHint": "輸入網址", - "copiedNotification": "已複製到剪貼簿" + "textFieldHint": "輸入網址" }, "menuName": "網格", "referencedGridPrefix": "檢視", From efb98d28ef2dcd8e90e41e9c34621b96a41adf42 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 8 Apr 2025 15:53:34 +0800 Subject: [PATCH 280/384] chore: rename data path --- .../lib/startup/tasks/rust_sdk.dart | 4 +- .../auth/backend_auth_service.dart | 5 +- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/flowy-core/Cargo.toml | 1 + frontend/rust-lib/flowy-core/src/config.rs | 55 +++++++++++++++---- .../rust-lib/flowy-user-pub/src/entities.rs | 1 + frontend/rust-lib/flowy-user/Cargo.toml | 3 - .../rust-lib/flowy-user/src/entities/auth.rs | 4 ++ .../rust-lib/flowy-user/src/event_handler.rs | 6 +- .../flowy-user/src/user_manager/manager.rs | 1 - frontend/rust-lib/lib-infra/src/file_util.rs | 2 +- 11 files changed, 58 insertions(+), 28 deletions(-) diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index 55f2f53512..c406dd161a 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -73,10 +73,10 @@ Future appFlowyApplicationDataDirectory() async { case IntegrationMode.develop: final Directory documentsDir = await getApplicationSupportDirectory() .then((directory) => directory.create()); - return Directory(path.join(documentsDir.path, 'data_dev')).create(); + return Directory(path.join(documentsDir.path, 'data_dev')); case IntegrationMode.release: final Directory documentsDir = await getApplicationSupportDirectory(); - return Directory(path.join(documentsDir.path, 'data')).create(); + return Directory(path.join(documentsDir.path, 'data')); case IntegrationMode.unitTest: case IntegrationMode.integrationTest: return Directory(path.join(Directory.current.path, '.sandbox')); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index 9147fb4fb9..b5cf413603 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -8,7 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/uuid.dart'; import '../../../generated/locale_keys.g.dart'; import 'device_id.dart'; @@ -65,13 +64,13 @@ class BackendAuthService implements AuthService { Map params = const {}, }) async { const password = "Guest!@123456"; - final uid = uuid(); - final userEmail = "$uid@appflowy.io"; + final userEmail = "anon@appflowy.io"; final request = SignUpPayloadPB.create() ..name = LocaleKeys.defaultUsername.tr() ..email = userEmail ..password = password + ..isAnon = true // When sign up as guest, the auth type is always local. ..authType = AuthenticatorPB.Local ..deviceId = await getDeviceId(); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index baa5c5ee6d..a72f4596f6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2604,6 +2604,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "url", "uuid", ] @@ -3049,7 +3050,6 @@ dependencies = [ "collab-user", "dashmap 6.0.1", "diesel", - "diesel_derives", "fake", "fancy-regex 0.11.0", "flowy-codegen", @@ -3064,7 +3064,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "once_cell", "protobuf", "quickcheck", "quickcheck_macros", @@ -3074,7 +3073,6 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_repr", "strum", "strum_macros 0.25.2", "tokio", diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 6225652078..6499f79284 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -58,6 +58,7 @@ serde_repr.workspace = true uuid.workspace = true sysinfo = "0.30.5" semver = { version = "1.0.22", features = ["serde"] } +url = "2.5.0" [features] profiling = ["console-subscriber", "tokio/tracing"] diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 0067eff5a1..27f658de8d 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -1,9 +1,10 @@ use std::fmt; -use std::path::Path; +use std::path::{Path, PathBuf}; use base64::Engine; use semver::Version; use tracing::{error, info}; +use url::Url; use crate::log_filter::create_log_filter; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; @@ -46,30 +47,60 @@ impl fmt::Debug for AppFlowyCoreConfig { } fn make_user_data_folder(root: &str, url: &str) -> String { - // Isolate the user data folder by using the base url of AppFlowy cloud. This is to avoid - // the user data folder being shared by different AppFlowy cloud. - let storage_path = if !url.is_empty() { - let server_base64 = URL_SAFE_ENGINE.encode(url); - format!("{}_{}", root, server_base64) + // If a URL is provided, try to parse it and extract the domain name. + // This isolates the user data folder by the domain, which prevents data sharing + // between different AppFlowy cloud instances. + print!("Creating user data folder for URL: {}, root:{}", url, root); + let mut storage_path = if url.is_empty() { + PathBuf::from(root) } else { - root.to_string() + let server_base64 = URL_SAFE_ENGINE.encode(url); + PathBuf::from(format!("{}_{}", root, server_base64)) }; + // Only use new storage path if the old one doesn't exist + if !storage_path.exists() { + let anon_path = format!("{}_anonymous", root); + // We use domain name as suffix to isolate the user data folder since version 0.8.9 + let new_storage_path = if url.is_empty() { + // if the url is empty, then it's anonymous mode + anon_path + } else { + match Url::parse(url) { + Ok(parsed_url) => { + if let Some(domain) = parsed_url.host_str() { + format!("{}_{}", root, domain) + } else { + anon_path + } + }, + Err(_) => anon_path, + } + }; + + storage_path = PathBuf::from(new_storage_path); + } + // Copy the user data folder from the root path to the isolated path // The root path without any suffix is the created by the local version AppFlowy - if !Path::new(&storage_path).exists() && Path::new(root).exists() { - info!("Copy dir from {} to {}", root, storage_path); + if !storage_path.exists() && Path::new(root).exists() { + info!("Copy dir from {} to {:?}", root, storage_path); let src = Path::new(root); - match copy_dir_recursive(src, Path::new(&storage_path)) { - Ok(_) => storage_path, + match copy_dir_recursive(src, &storage_path) { + Ok(_) => storage_path + .into_os_string() + .into_string() + .unwrap_or_else(|_| root.to_string()), Err(err) => { - // when the copy dir failed, use the root path as the storage path error!("Copy dir failed: {}", err); root.to_string() }, } } else { storage_path + .into_os_string() + .into_string() + .unwrap_or_else(|_| root.to_string()) } } diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 857a735edb..fce59b7994 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -42,6 +42,7 @@ pub struct SignUpParams { pub password: String, pub auth_type: Authenticator, pub device_id: String, + pub is_anon: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 5ad579a0fb..4d021161ac 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -31,12 +31,9 @@ tracing.workspace = true bytes.workspace = true serde = { workspace = true, features = ["rc"] } serde_json.workspace = true -serde_repr.workspace = true protobuf.workspace = true lazy_static = "1.4.0" diesel.workspace = true -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } -once_cell = "1.17.1" strum = "0.25" strum_macros = "0.25.2" tokio = { workspace = true, features = ["rt"] } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index dbfd9b811a..09b71eb6d4 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -57,6 +57,9 @@ pub struct SignUpPayloadPB { #[pb(index = 5)] pub device_id: String, + + #[pb(index = 6)] + pub is_anon: bool, } impl TryInto for SignUpPayloadPB { @@ -73,6 +76,7 @@ impl TryInto for SignUpPayloadPB { password: password.0, auth_type: self.auth_type.into(), device_id: self.device_id, + is_anon: self.is_anon, }) } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index dfd166b1a6..4d2ee57ce8 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -76,14 +76,14 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let authenticator = params.auth_type.clone(); - let old_authenticator = manager.cloud_services.get_user_authenticator(); + let prev_authenticator = manager.cloud_services.get_user_authenticator(); match manager.sign_up(authenticator, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { manager .cloud_services - .set_user_authenticator(&old_authenticator); - return Err(err); + .set_user_authenticator(&prev_authenticator); + Err(err) }, } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index c41485f6c9..864629d169 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -403,7 +403,6 @@ impl UserManager { ) -> Result { // sign out the current user if there is one let migration_user = self.get_migration_user(&authenticator).await; - self.cloud_services.set_user_authenticator(&authenticator); let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; diff --git a/frontend/rust-lib/lib-infra/src/file_util.rs b/frontend/rust-lib/lib-infra/src/file_util.rs index 6de14b2304..b088256c26 100644 --- a/frontend/rust-lib/lib-infra/src/file_util.rs +++ b/frontend/rust-lib/lib-infra/src/file_util.rs @@ -10,7 +10,7 @@ use zip::write::FileOptions; use zip::ZipWriter; use zip::{CompressionMethod, ZipArchive}; -pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { +pub fn copy_dir_recursive(src: &Path, dst: &PathBuf) -> io::Result<()> { for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { let path = entry.path(); let relative_path = path.strip_prefix(src).unwrap(); From d1d598940d1ee76b8e1afddc5a9e72566f25e4fc Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 8 Apr 2025 16:05:52 +0800 Subject: [PATCH 281/384] chore: clippy --- .../lib/user/application/auth/backend_auth_service.dart | 1 - frontend/rust-lib/flowy-user-pub/src/entities.rs | 1 - frontend/rust-lib/flowy-user/src/entities/auth.rs | 4 ---- 3 files changed, 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index b5cf413603..4d03788c8c 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -70,7 +70,6 @@ class BackendAuthService implements AuthService { ..name = LocaleKeys.defaultUsername.tr() ..email = userEmail ..password = password - ..isAnon = true // When sign up as guest, the auth type is always local. ..authType = AuthenticatorPB.Local ..deviceId = await getDeviceId(); diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index fce59b7994..857a735edb 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -42,7 +42,6 @@ pub struct SignUpParams { pub password: String, pub auth_type: Authenticator, pub device_id: String, - pub is_anon: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 09b71eb6d4..dbfd9b811a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -57,9 +57,6 @@ pub struct SignUpPayloadPB { #[pb(index = 5)] pub device_id: String, - - #[pb(index = 6)] - pub is_anon: bool, } impl TryInto for SignUpPayloadPB { @@ -76,7 +73,6 @@ impl TryInto for SignUpPayloadPB { password: password.0, auth_type: self.auth_type.into(), device_id: self.device_id, - is_anon: self.is_anon, }) } } From 23f2d85e708dba315f6ad3d3c4b989b31351ce46 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 8 Apr 2025 20:23:25 +0800 Subject: [PATCH 282/384] chore: clippy --- frontend/rust-lib/lib-infra/src/file_util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/lib-infra/src/file_util.rs b/frontend/rust-lib/lib-infra/src/file_util.rs index b088256c26..6de14b2304 100644 --- a/frontend/rust-lib/lib-infra/src/file_util.rs +++ b/frontend/rust-lib/lib-infra/src/file_util.rs @@ -10,7 +10,7 @@ use zip::write::FileOptions; use zip::ZipWriter; use zip::{CompressionMethod, ZipArchive}; -pub fn copy_dir_recursive(src: &Path, dst: &PathBuf) -> io::Result<()> { +pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { let path = entry.path(); let relative_path = path.strip_prefix(src).unwrap(); From 6886261692ac9854a5fdb68602de0d5a63192f23 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 8 Apr 2025 20:49:15 +0800 Subject: [PATCH 283/384] chore: ensure path --- frontend/rust-lib/flowy-core/src/config.rs | 18 ++++++++++++++++++ frontend/rust-lib/flowy-core/src/lib.rs | 2 ++ 2 files changed, 20 insertions(+) diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 27f658de8d..2bad578627 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -29,7 +29,25 @@ pub struct AppFlowyCoreConfig { pub(crate) log_filter: String, pub cloud_config: Option, } +impl AppFlowyCoreConfig { + pub fn ensure_path(&self) { + let create_if_needed = |path_str: &str, label: &str| { + let dir = std::path::Path::new(path_str); + if !dir.exists() { + match std::fs::create_dir_all(dir) { + Ok(_) => info!("Created {} path: {}", label, path_str), + Err(err) => error!( + "Failed to create {} path: {}. Error: {}", + label, path_str, err + ), + } + } + }; + create_if_needed(&self.storage_path, "storage"); + create_if_needed(&self.application_path, "application"); + } +} impl fmt::Debug for AppFlowyCoreConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("AppFlowy Configuration"); diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 1562c44a22..79a889f86f 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -105,6 +105,8 @@ impl AppFlowyCore { #[instrument(skip(config, runtime))] async fn init(config: AppFlowyCoreConfig, runtime: Arc) -> Self { + config.ensure_path(); + // Init the key value database let store_preference = Arc::new(KVStorePreferences::new(&config.storage_path).unwrap()); info!("🔥{:?}", &config); From 145d1e5fdb1d2b707f69ce3e9ac33db33f3133cb Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 9 Apr 2025 09:36:53 +0800 Subject: [PATCH 284/384] feat: support white label on windows (#7706) --- .../shared/error_page}/error_page.dart | 34 ++-- .../notification/notification_service.dart | 7 +- .../pages/settings_shortcuts_view.dart | 2 +- .../widgets/setting_appflowy_cloud.dart | 2 +- .../theme_upload_learn_more_button.dart | 2 +- .../scripts/white_label/i18n_white_label.sh | 103 ++++++++++++ .../scripts/white_label/icon_white_label.sh | 84 ++++++++++ .../white_label/resources/my_company_logo.ico | Bin 0 -> 67646 bytes .../white_label/resources/my_company_logo.png | Bin 0 -> 4121 bytes .../white_label/resources/my_company_logo.svg | 1 + frontend/scripts/white_label/white_label.sh | 118 ++++++++++++++ .../white_label/windows_white_label.sh | 151 ++++++++++++++++++ 12 files changed, 481 insertions(+), 23 deletions(-) rename frontend/appflowy_flutter/{packages/flowy_infra_ui/lib/widget => lib/shared/error_page}/error_page.dart (88%) create mode 100644 frontend/scripts/white_label/i18n_white_label.sh create mode 100644 frontend/scripts/white_label/icon_white_label.sh create mode 100644 frontend/scripts/white_label/resources/my_company_logo.ico create mode 100644 frontend/scripts/white_label/resources/my_company_logo.png create mode 100644 frontend/scripts/white_label/resources/my_company_logo.svg create mode 100644 frontend/scripts/white_label/white_label.sh create mode 100644 frontend/scripts/white_label/windows_white_label.sh diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart b/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart similarity index 88% rename from frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart rename to frontend/appflowy_flutter/lib/shared/error_page/error_page.dart index d395873bd7..9661fd822a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart +++ b/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart @@ -1,14 +1,18 @@ import 'dart:io'; +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +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:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:url_launcher/url_launcher.dart'; class FlowyErrorPage extends StatelessWidget { factory FlowyErrorPage.error( @@ -86,7 +90,9 @@ class FlowyErrorPage extends StatelessWidget { Listener( behavior: HitTestBehavior.translucent, onPointerDown: (_) async { - await Clipboard.setData(ClipboardData(text: message)); + await getIt().setData( + ClipboardServiceData(plainText: message), + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -188,8 +194,8 @@ class StackTracePreview extends StatelessWidget { "Copy", ), useIntrinsicWidth: true, - onTap: () => Clipboard.setData( - ClipboardData(text: stackTrace), + onTap: () => getIt().setData( + ClipboardServiceData(plainText: stackTrace), ), ), ), @@ -252,18 +258,14 @@ class GitHubRedirectButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyButton( leftIconSize: const Size.square(_height), - text: const FlowyText( - "AppFlowy", - ), + text: FlowyText(LocaleKeys.appName.tr()), useIntrinsicWidth: true, leftIcon: const Padding( padding: EdgeInsets.all(4.0), child: FlowySvg(FlowySvgData('login/github-mark')), ), onTap: () async { - if (await canLaunchUrl(_gitHubNewBugUri)) { - await launchUrl(_gitHubNewBugUri); - } + await afLaunchUri(_gitHubNewBugUri); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart index 5418eb2b1c..7a19e2a822 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart @@ -1,9 +1,8 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; - import 'package:local_notifier/local_notifier.dart'; -const _appName = "AppFlowy"; - /// Manages Local Notifications /// /// Currently supports: @@ -13,7 +12,7 @@ const _appName = "AppFlowy"; /// class NotificationService { static Future initialize() async { - await localNotifier.setup(appName: _appName); + await localNotifier.setup(appName: LocaleKeys.appName.tr()); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart index 3552205ba4..0d3716c7dc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart'; +import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; @@ -21,7 +22,6 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index a9cb362bf3..f423156025 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -3,6 +3,7 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/shared/share/constants.dart'; +import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; @@ -19,7 +20,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index 9e25dece0e..bdc5ef0546 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -1,12 +1,12 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.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/buttons/secondary_button.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { diff --git a/frontend/scripts/white_label/i18n_white_label.sh b/frontend/scripts/white_label/i18n_white_label.sh new file mode 100644 index 0000000000..60152d1630 --- /dev/null +++ b/frontend/scripts/white_label/i18n_white_label.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --company-name Set the custom company name" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --company-name \"MyCompany Ltd.\"" +} + +CUSTOM_COMPANY_NAME="" +I18N_DIR="resources/translations" + +while [[ $# -gt 0 ]]; do + case $1 in + --company-name) + CUSTOM_COMPANY_NAME="$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$CUSTOM_COMPANY_NAME" ]; then + echo "Error: Company name is required" + show_usage + exit 1 +fi + +if [ ! -d "$I18N_DIR" ]; then + echo "Error: Translation directory not found at $I18N_DIR" + exit 1 +fi + +echo "Replacing 'AppFlowy' with '$CUSTOM_COMPANY_NAME' in translation files..." + +if sed --version >/dev/null 2>&1; then + SED_INPLACE="-i" +else + SED_INPLACE="-i ''" +fi + +echo "Processing translation files..." +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + # Check if directory exists and has JSON files + if [ ! -d "$I18N_DIR" ] || [ -z "$(ls -A "$I18N_DIR"/*.json 2>/dev/null)" ]; then + echo "Error: No JSON files found in $I18N_DIR directory" + exit 1 + fi + + # Process each JSON file in the directory + for file in "$I18N_DIR"/*.json; do + echo "Updating $(basename "$file")" + # Use jq to replace AppFlowy with custom company name in values only + if command -v jq >/dev/null 2>&1; then + # Create a temporary file for the transformation + jq --arg company "$CUSTOM_COMPANY_NAME" 'walk(if type == "string" then gsub("AppFlowy"; $company) else . end)' "$file" > "${file}.tmp" + # Check if transformation was successful + if [ $? -eq 0 ]; then + mv "${file}.tmp" "$file" + else + echo "Error: Failed to process $file with jq" + rm -f "${file}.tmp" + exit 1 + fi + else + # Fallback to sed if jq is not available + # First, escape any special characters in the company name + ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') + # Replace AppFlowy with the custom company name in JSON values + sed $SED_INPLACE 's/\(".*"\): *"\(.*\)AppFlowy\(.*\)"/\1: "\2'"$ESCAPED_COMPANY_NAME"'\3"/g' "$file" + if [ $? -ne 0 ]; then + echo "Error: Failed to process $file with sed" + exit 1 + fi + fi + done +else + for file in $(find "$I18N_DIR" -name "*.json" -type f); do + echo "Updating $(basename "$file")" + # Use jq to only replace values, not keys + if command -v jq >/dev/null 2>&1; then + jq 'walk(if type == "string" then gsub("AppFlowy"; "'"$CUSTOM_COMPANY_NAME"'") else . end)' "$file" > "$file.tmp" && mv "$file.tmp" "$file" + else + # Fallback to sed with a more specific pattern that targets values but not keys + sed $SED_INPLACE 's/: *"[^"]*AppFlowy[^"]*"/: "&"/g; s/: *"&"/: "'"$CUSTOM_COMPANY_NAME"'"/g' "$file" + # Fix any double colons that might have been introduced + sed $SED_INPLACE 's/: *: */: /g' "$file" + fi + done +fi + +echo "Replacement complete!" diff --git a/frontend/scripts/white_label/icon_white_label.sh b/frontend/scripts/white_label/icon_white_label.sh new file mode 100644 index 0000000000..eb45d0f02f --- /dev/null +++ b/frontend/scripts/white_label/icon_white_label.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --icon-path Set the path to the application icon (.svg file)" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --icon-path \"/path/to/new/icon.svg\"" +} + +NEW_ICON_PATH="" +ICON_DIR="resources/flowy_icons" +ICON_NAME_NEED_REPLACE=("flowy_logo.svg" "flowy_ai_chat_logo.svg" "flowy_logo_dark_mode.svg" "flowy_logo_text.svg") + +while [[ $# -gt 0 ]]; do + case $1 in + --icon-path) + NEW_ICON_PATH="$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$NEW_ICON_PATH" ]; then + echo "Error: Icon path is required" + show_usage + exit 1 +fi + +if [ ! -d "$ICON_DIR" ]; then + echo "Error: Icon directory not found at $ICON_DIR" + exit 1 +fi + +echo "Replacing icon..." + +echo "Processing icon files..." +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + for subdir in "${ICON_DIR}"/*/; do + if [ -d "$subdir" ]; then + echo "Checking subdirectory: $(basename "$subdir")" + for file in "${subdir}"*.svg; do + if [ -f "$file" ] && [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then + echo "Updating: $(basename "$subdir")/$(basename "$file")" + cp "$NEW_ICON_PATH" "$file" + if [ $? -eq 0 ]; then + echo "Successfully replaced $(basename "$file") in $(basename "$subdir") with new icon" + else + echo "Error: Failed to replace $(basename "$file") in $(basename "$subdir")" + exit 1 + fi + fi + done + fi + done +else + for file in $(find "$ICON_DIR" -name "*.svg" -type f); do + if [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then + echo "Updating: $(basename "$file")" + + cp "$NEW_ICON_PATH" "$file" + + if [ $? -eq 0 ]; then + echo "Successfully replaced $(basename "$file") with new icon" + else + echo "Error: Failed to replace $(basename "$file")" + exit 1 + fi + fi + done +fi + +echo "Replacement complete!" diff --git a/frontend/scripts/white_label/resources/my_company_logo.ico b/frontend/scripts/white_label/resources/my_company_logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..c922a6b36d45bbab0da4d142aa63e53757a04eed GIT binary patch literal 67646 zcmeHQ+mB?&U9Rrw*|}`&>?O7zVteP(b4eCy1r|5Ic#`$*&h&K8+F-{-2rVLnyr3W< zUf^Zs!2wG=#1evljFbh%iHQmR16Wud>|iB4geZ_G3c*em*;xcpmIdr(cbo64s;~R> znbUpF={`Nv(^F^l+d6eF^}Bq(y8NoD&lpp}znvX}|L4r=bjjRrj9CTY5--~8JmuN{ zJnG2h!~eko!2-bo!2-bo!2-bo!2-bo!2-bo!2-bo!2-bo<7I(bwPH4_vu1M%RGoI` z)frO*)t5lHcWz;Fyp#^w0B>SVoTn|}d;(Mfl|AvC*X(mlx2jXYa${hD`hvsWV=KHZ z&QdRed&m9y{EQw6=>fFB)*|XW+VxEm>Uw=~wzR%7MT{?i{uuN(p8f>sdVOgc_hw5t z=QFO&SeZV6ng@|B_O{KpII3-b2)O<&s0q63)9Cr%;o6)V8=yac|B!0I2M&}&#jt1J z&bqt;dJ{yv|IgEHq$K5e@B5%t5Z|c;A2?9Zn`G=~iT(FL!k%do(GPh28t4O{-~$H; z{tVc&t|#Fhy{2b^4;()12LkrN2M&M_6x81f>uzc23r5fv_6U3VMDT$T z@__=ezk_&yB%FJt!3Rdb2MWfXbAY7a10#TKng|7Aue$slU{7Mb*R@`4K=e7z4}^Kb zG|&pVKLqUm4eLS+nBScTj>IyGZFV163iE^oLEN>NAz$#R6d|+_giYQ?>_Uw~~i@oFy zJ|Ii%Pb%!|+H=Enab_9gGVR+byxzw(4L%UJ$lm04VZVqzd2xCP*lWMu(6N`M!3SiC zy-7z2dubeeK$aM8I*Qm!wKQrbB*O7VlPdC56BYv(s96E8V4VcC9}zQA zLHjbdlTG6Er-Bcp+lWt$Blglf_<$^toQ^a0(m42lEYX=B2JEGA@BvxErHuuz>X7^LzYv;`(k2cKHDLFIpX_OFw(dzwh6t z4%(kMa^E8o_N}>{HunAQ-RRWY*)f;iaqRIOu<{dgQ}lsPg5L6Y==Wd5hSxrM?#HG| zc$eS*;W-J*cN7$J8yT|4?CPT+wZ9cx;n)UEw4)yotjb!Yj zm?xbV=L2K9w|vjA&+*;GIQFP}N%cMn@1$$N2f9?c1A#rqZf*=0ZS#|G?sYBrK+FdY z0``bOO6%_Xy@OyczX(3yePF}IAFcKKQJB|@xA_HQAABHSf9QRH@jT)ek(@ z{T|hOVXw4jUr--pe!$tc_}Fkcd+ZeJ>TNdO=6gRF7wq-jC&v9e7tX!Xm=7G~93kfl zZT%QYAIK3yMSVbjGsWx1xijiG2=<%<@A*62DF38$`?nn0ZBd#1J?8dk|GZ;8&HKKK zcS*|k!d~}UK7cjFGS-hed;oDrlIV%ocFhw~w?+G%dfJNo`uW}4s-1Ug&pBD&OcbEY z#aR1V1oqdU%LiI=X>KQnZ{mIRK};?7eU9^0?`hv(0{tS0=Xu%zeJB3qt?H5TMz!E} zV6ZhKXKQCK_cttX{`f-nzh?wn$kuAxs<`&1w^Lr9?5y{`>^$@NN%ejm>p768pL#F( ze=VZt$GvWOd|-I(fU6%|0~w?p;CRB;70hEJo$XTFJ1?)V;90k3?$*JpGw(w=-}3g6 z->WU`mdeJIf7e>hW{x#%uS3fpF~47`b+-9gWn#UTT*7`5*GMd1KEP-AY&YGa*V{g@ zfW83x3h`F`D1YRvwFearM5!F^=D}Qt`ax19{p3`QOWp#i>#80bzx;OxTcfq5PYN z8OA2At*~pQwE2fX#F+L-!a9DfH}*N=5y}dlBGxI@Ew%Zwdt8QiKB?YUYQUbp7~wI9 z56IpxH?VG4i;o8~*j@E<2<^aGuj2&Mw8OXk_E|iy^>X1Fw{0}9X9jNb{XP<51K$Pq z3x)Of4txJMA+2rNz83cZ>XfxFkUydw7>T|>c)VnFep%T3jJNB=ZZ4fy@SN~}66eHUbvxSD>s;l37tgilcAT%7 zJmY*yfigtcpF5*{c*0(FOT4?Klu0scoZr#rcWT_uS^jLZoE(GrKu2HTeSqT^`?sds z9MjKw_`tuQJla4QB-ulD_&Kjr7W}36FZl9RMpF6G^Y?uDN9?#QXPZweJpG`twm~_i z3mB`SA1Ermx8wZHSexI?^3=oj1I`a*3&jPrMK60tZ6w=#5pm!|ZT=|ziu)4G)2F*} zPre@Wfg_X=M4OYJ_(XZ@T-bfozf(!((7uZ2RR7QW@_V&0vc(Va?7W{h#M~V5%QoL2 z&XK>#4%|8ZnE4~mfAD(WrOg-bcnZF~`hq+@Ala!4bee^3{2S!$u}7SDQl7T-HesEh zZRSC=IdK*S!aSM^y9;=Z?`5g~;<1S5-vOoZkdM6}9uCWVI#O@S=gVM!MZS_hs#DTA zZ6G;1+WZa1c{%M(S@@29*Ug2*+X2ee%kEJbrNd9-9qL$S`FRe$*Kl=0_MxrGLpn$K z>0I)0eWS`UK&ia>+@r62flVcZAEQ#O_?NvU*rmgRwLIay`mJKmP6m$b3$oy1SqgxNoA zJ$$eE$tT_TDvd9H4<#9q{w9d!Ne7i3Pi6lr4bC&c&uBNKY3y3}`7B8O{RNzphsurf zQ$87cwy}NI`<6id#NrRxygvQ-fOOzIYD>pBpKf`+<9TzdP~pNu_mXT48|@w(YK?K<^g z8xWH*>zGhF{DHSiCLAaepZD|Asux+t*OpoPU5mV1M7{ zZ0dfC*IqY_$|hMUJ2CltygR}7agIGJ>Fa_TKjC@VkTzvmvazs^r?SnL8*_-ESB{V; z#t`W|WrqfyJ-d3$`HhqLtmjR=;{2j{-TzNKcZz&LCH4dSjdibs@o{XNQ^mXA0kM4P zr1I(CzXD?U?YW%-+x%{_TAsV-h$u^3M&-YadyDoxH>Y8?7Vha*yuGg9rQ;p-!?Y!B zN?XVVJWnF=eH`~Kh96yJJae=s&-hKgyTx%-+iySb`KfJ5;>~Ni|AP4IH~4WL@w|VY zGIQ>i=}8dhIsO^+AE57nI3M^(tKOq7u|5^q-^b;O=fS@~SmE0>h|1U2` znh)|m=~0lyvjH1%KWVSS+mX*ZACUhq**1`4(P=v#a{E)T);`t3p;x)XxM%wU`hkm= zAdB}y$Ot*#i}@O?w^rS-j6WoyF$#i<=3E6Pf#9?8TR|Ap>A<(Me^-@OV+0`_PWS63i4(-pd-rYX!mo+d*~NW*uD|_r*lP^ct?ui7YmXn} zd}3AfQ3LjV45(OUpm=bL@em~Q7ro?K{cn`jF!ney7 zllnu&jr)@9#<69?jxR$zm}H-<*9waFe%1L9)|TwPi(=xvG#&gn9&=xqW0()|V4A&> z?z7Gg8t*;1SShbB+20=9vXb}=;~MvM*H7PTV-4?rAs)J2LdmFY58IAcf!0jv|3E({nC za(&;oZ)2Bw{55x)cYW`*jVk^&h5O!^wTE_eE<2}E}XynXc z+W_a=TF1(>K1kpAU(hQcu4Vqkm=bmSD-d<;Bwx+lu$N4+q~E8aeza5acwQ z{l$aSS^7%Rs5jGbB5_^^9_y#H$IrSKB#uBo9^Lydp|GNRBb!@#N6n`d#OGE*&B=p+x>vP=ku;_ z+XB|RV*uI4e=lfeqA}uQzHWZNHo)%_E`50MJ)GN}0`6Bl_P3CRwxEqaZ9$u`PF$g_ z&bqZ7x2G$|x7)knu^-nXW5E)C|A}>P&UrJ&r=HZAeI3WdJZDUFn20(n9^g4`@(OHr z)c?&LV9&Ymz1h<>4FAC2+-3VO;`ijs_4hr@{3k$ffv9tqSeVa^BT0AK;uF=$llVQW z{jIRph|K!xaQAZ#1NUf|=a(wNpDFdi{4o4_iNBGa`DM_TK;Hsg2mKJl-wKU0QAd6s zfZuZYOVB0Idwrc}%-q6$Ycch6jZssN?79E8ThtG%o*yzBSTj6=eWXXQCN_O9bmx9@ z{?4E2IFfX`7rz^y;Qr4?79OC_wwX+^unFB^3`bm)=ueCH2>em zyb{e{H|CjW{u{=ej^`MZ~mi3)IM&6ubFw|0z)3IO&U0)Tyo0N*gB zou~jWnetjxfG5o4<%j@aKZ{G-k_hbY!z)*n2m8|~@{;mk{|s2REWmSK(|tb6ca&#Y zEK_;%B2S-pr)>e;qiq3PQEdSntF{2{tSx|hvMqossV#ux)Dl1^5+)!vwv1<2=?F_%1Fo7^f(4L2*}P`Tmu{_@mFDZrAII~Rt9RyLvFFU08)gQVXSVM?j|ZPO zzj4D%BY&dVTu1(xfjpDjubZW2^K<4TbieqnDQ~}RHk-|_n+NW$na{ohdEYjlX*Pdg z9)K-Q-QFoZcCB=#*}S>4g7fX$Yo$}y%a@wXTWim}vr~SI@7#RuYO{Ig_!*XkGCzIm zC$^i-yY)*y0@`VG|FiAurqTq;E6nk@#wrl0CxEUAVNbfdJbj?qd=(1%<2kd) zmu8fIo<&`Ug8n$hDo6!gp6WH>6=mhuSY@f8yHE9+O)BW}R1bM5=<-w#dO$&!r+Uak zL6^7cHLZg7f>ke~pjD3w&KY}Ds2;UC=JL}{(^sYN>r{^lwhn74IJgem!rV~KP(3Qx zI-njEY#pbVzrCZpQ}p(l@>I_%=r1@!K|z0bL#`?B`wY0QJK#k;{&B};OTX{(H_vpY OaDR7Pb=Sx0|NjT{45)Gd literal 0 HcmV?d00001 diff --git a/frontend/scripts/white_label/resources/my_company_logo.png b/frontend/scripts/white_label/resources/my_company_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8f50872743ff4611e9e205744c1446593a671fab GIT binary patch literal 4121 zcmbtWdpMKrAKzBXHixo=M2&4EMM4LMMmfyJ97fWKPUvW*-U-=WIyfa!5kp5S(e#!? zQI4I&Ya0moK z(UEHFjzFNm6otUd0AEkCNYB6`$JNQh4lEJY)>smKDu`1P47@|bkeskYCoCAJ0SE#B z)8HjpHj?N#5c&>aR3O@eQJz4<=}aCuPBD;SPB|Sw<%0X0UstwCWWYQCKnAsBSa|FrYD9tg@-XD zC&k5XSh7qD(Fq60$ut}Z4$Pz$GEH8a0cv6NoMrOqSX~&5(*a8-P)mX243=0OXV5V0 z%#$WAy=mrPSKa8T5hrNBLd4grQX>8An^1)2w*fO5c`NoFEQhtzei%z6Cl)<|M#?iL67|naP+1c4VipnM6k*ZT7-R(_kcU52weFDFwp8(izCd z$gTikXb%`>$@-jb67CjcAwY_(?qms^j2~g(373F-m{ddumohn5*0F^XWhppV5tzeB zK!VVKJp^+&okWudxw0FXWQIpzvaBE2mc>+%2+AQ4Tnv#0-cUFZ;d#}k<`7tP9C2qb z5i_%)TUp~{TQ+R=@|f`aj0yhEln$lO=cx{=n^tPxOymC|YekGS*hY~y#d>eO-C3W6 z&dDX#Jrc!94G9$z3=bem>c*yhI`ayU)c47h6CE+A6A|Y=84^UT-32(j-zms+H^XX$}e&HHFR7Hl9wzVy>hze)K10S{lgbdd{ww3Ui-33$@kmrjBP(x zeDNud#2x!Mt8DqkH8mGME!1giC8S(NB_prRP(d2=X=<17ke;n)e(sx~e(s z+{8XX-u~ysA}34#mxcXC!lQK)7NVh*CIa|};lF%rSUnw!a*mDPbx7M}hL1_}+cpv0 zemrS2%jiH_z2TBZs^hY~Eq^)dWF42Z&CpmQhf7{X<#{Y@xpq0*L~jEvEY8Ye;Sh9V z=tVwWZ7i$c>O|P4pW>t_^?blrRDuX%U6bfCKjqaYF=(g-2T^` zg66xf5xeNP{ARbu3{*#Lc&*?)e?+W`dCc1RZ+yP1_5 zr-O|tc;S|sphhCB;47z|=VXyJ{OYB4Wts{a;>wlxu5W} zyuQ4SJ47PrQ&EW#B;R7wB<^(rR^3F-(b_#>CJF6W7QAL1d_>SsNxoka|9A^Hrs5L0 z*L|gK*RkH4)X{V^Z8f_@F43N*a=&zULWD6%xEqhXF-u?F2DRF{%>s29HuwY;64KEIzx=aGSiDS03X^37 zbHr!TP*v#>Ce*TL%z;RfZ;O8L(Nq~CiaBuM+O!$do}AZjS`XumWTn-EaAIAUcZzDk9A}Ca zOtDRBP+(*!xHmM;)Qqg}h#Ovs-sfoTGHnzq$}x$1i0^BH?Fz(6*Pr68H#)~%%Ln9a zTzC|$OGdd0EztYB@A8(QgqgLmQx0{iSUQ0_F4UXwa;Dzuh{_(V&rmm}RR^Yr_84t#H23hj!xa&uH}tK}7O1|D;=gT?C|jLPcUfwbz++Vq z(4X{#`x0m1#Ni%B*Y6Bgf6?|llN+Ml|DI2lE55iIeYOZP5C+sB7iC^Qeiu4681J8U z-{|q9501F-jfZRV9q-y z0(;9HZ~8t^-k9ugzSOP~R%L_o`{SE+T$gHKm-zLc0r!2%o++m$JUy1lb?GiUax_$2 znk@Jlid)D|;J)!tR2AOfEb|c)(3y4pGsE&Wfek+Aoz(>;_dK#-E-N>?9A!Bq*wo>L z8_nbgV8pdiB@ZN2yx-w8lMI;Zj{;zuD%ZqT_3!lI!N0 z*M*7)@dGRF`W5&&`=72Cl_tBGQ_HJy-%XKUPiN0!b5DbX3J?#OvWx|@6jnL(n^PHw-_n8vxi2cLiRh^nm>|0-C##rN- zgO3TL85{EiAL;^;)d7diEi^5mq=v4}f{1xNPk9T{!<0RC6=m0G2_?V0tQLRikiWX1 zl2>St4XF2ew;#E7tWl&nv4NGSZc^PV;0|lLJwXTe`anANO_1k6L1$fCcLDcy_uHtK zO05}CiDm$MXmhzk<#|4DLX~wX^)vEu24^B8WqW1>|7pT{i}Vtu&a~_-HaAn`Gi2v4 zIHSK!+i$$$2Vf4nz3xmu!l=G9ey{TSUTDcf_u7;I9(TCbU4kCz@}cNvHSvYpjN9`L zp*}D-Z@-I}JrO84GV&p1QS^My3K#28v0E%USmeVotZL%>Cad4QT=})fSG6|Zn;R(Q zyeCvOsf8R%OJP;@_4w*~d|X;)1dXwy(8Kzx9h^pPwy%mXyu-{F*!-N}n@joet%T9I zl)Tr5$iDg8b_B_d1WD!})3A-g(6~&l4*U&7O7{HZh z^|!7?pN+*1rJA*e>rU$$OsO-N+O!xy`qZ`&p_AoNH~oEaqbBOI2eOZ!@_WgMm@7k zDy4jFo)C;KxVy8Oq1dAo)jR$X+Qq&&>jC%Xc+c$}FLFao^Z3@bKTE}}r1x8{UipL^ z) z$9oPWnk;mi-Or`xh`GZ0QpP#W@4JHKF`uBqxQ>L{u7pgKv=tIle$-mDeL`wyLa{j8 zjb*;_WfWO(YURr~z2L#WZhJ?VQ2V7-_RAl6N@_IxZW8d%kt41t7Ku;;^}iE~Mp36O z2s6>Y7nEM~{yPmew+x5dUw8 z>VD^)`nMNw(U)nKMMhTpOyyU8m)5fibGOK~d+k}9#VzbWY9;ikN#>udi}dP3Se8V( zEIzWLx?nHGa@^t$q~7$P?Py=5*Yn}QX4`d7F3z7fO7Y0q+W1n0pDVw1up!RwU3T`h vgQ-wMvi \ No newline at end of file diff --git a/frontend/scripts/white_label/white_label.sh b/frontend/scripts/white_label/white_label.sh new file mode 100644 index 0000000000..2d6004cf9d --- /dev/null +++ b/frontend/scripts/white_label/white_label.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Default values +APP_NAME="AppFlowy" +APP_IDENTIFIER="com.appflowy.appflowy" +COMPANY_NAME="AppFlowy Inc." +COPYRIGHT="Copyright © 2025 AppFlowy Inc." +ICON_PATH="" +WINDOWS_ICON_PATH="" +PLATFORMS=("windows" "linux" "macos" "ios" "android") + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --app-name Set the application name" + echo " --app-identifier Set the application identifier" + echo " --company-name Set the company name" + echo " --copyright Set the copyright information" + echo " --icon-path Set the path to the application icon (.svg)" + echo " --windows-icon-path Set the path to the windows application icon (.ico)" + echo " --platforms Comma-separated list of platforms to white label (windows,linux,macos,ios,android)" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" + echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" + echo " --platforms \"windows,linux,macos\" \\" + echo " --windows-icon-path \"./assets/icons/mycompany.ico\" \\" + echo " --icon-path \"./assets/icons/mycompany.svg\"" +} + +while [[ $# -gt 0 ]]; do + case $1 in + --app-name) + APP_NAME="$2" + shift 2 + ;; + --app-identifier) + APP_IDENTIFIER="$2" + shift 2 + ;; + --company-name) + COMPANY_NAME="$2" + shift 2 + ;; + --copyright) + COPYRIGHT="$2" + shift 2 + ;; + --icon-path) + ICON_PATH="$2" + shift 2 + ;; + --windows-icon-path) + WINDOWS_ICON_PATH="$2" + shift 2 + ;; + --platforms) + IFS=',' read -ra PLATFORMS <<< "$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$APP_NAME" ] || [ -z "$APP_IDENTIFIER" ] || [ -z "$COMPANY_NAME" ] || [ -z "$COPYRIGHT" ] || [ -z "$ICON_PATH" ]; then + echo "Error: All parameters are required" + show_usage + exit 1 +fi + +if [ ! -f "$ICON_PATH" ]; then + echo "Error: Icon file not found at $ICON_PATH" + exit 1 +fi + +if [ ! -f "$WINDOWS_ICON_PATH" ]; then + echo "Error: Windows icon file not found at $WINDOWS_ICON_PATH" + exit 1 +fi + +run_platform_script() { + local platform=$1 + local script_path="scripts/white_label/${platform}_white_label.sh" + + if [ ! -f "$script_path" ]; then + echo -e "\033[31mWarning: White label script not found for platform: $platform\033[0m" + return + fi + + echo -e "\033[32mRunning white label script for $platform...\033[0m" + bash "$script_path" \ + --app-name "$APP_NAME" \ + --app-identifier "$APP_IDENTIFIER" \ + --company-name "$COMPANY_NAME" \ + --copyright "$COPYRIGHT" \ + --icon-path "$WINDOWS_ICON_PATH" +} + +echo -e "\033[32mRunning i18n white label script...\033[0m" +bash "scripts/white_label/i18n_white_label.sh" --company-name "$COMPANY_NAME" + +echo -e "\033[32mRunning icon white label script...\033[0m" +bash "scripts/white_label/icon_white_label.sh" --icon-path "$ICON_PATH" + +for platform in "${PLATFORMS[@]}"; do + run_platform_script "$platform" +done + +echo -e "\033[32mWhite labeling process completed successfully!\033[0m" diff --git a/frontend/scripts/white_label/windows_white_label.sh b/frontend/scripts/white_label/windows_white_label.sh new file mode 100644 index 0000000000..58801424ff --- /dev/null +++ b/frontend/scripts/white_label/windows_white_label.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +APP_NAME="AppFlowy" +APP_IDENTIFIER="com.appflowy.appflowy" +COMPANY_NAME="AppFlowy Inc." +COPYRIGHT="Copyright © 2025 AppFlowy Inc." +ICON_PATH="" + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --app-name Set the application name" + echo " --app-identifier Set the application identifier" + echo " --company-name Set the company name" + echo " --copyright Set the copyright information" + echo " --icon-path Set the path to the application icon (.ico file)" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" + echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" + echo " --icon-path \"./assets/icons/company.ico\"" +} + +while [[ $# -gt 0 ]]; do + case $1 in + --app-name) + APP_NAME="$2" + shift 2 + ;; + --app-identifier) + APP_IDENTIFIER="$2" + shift 2 + ;; + --company-name) + COMPANY_NAME="$2" + shift 2 + ;; + --copyright) + COPYRIGHT="$2" + shift 2 + ;; + --icon-path) + ICON_PATH="$2" + shift 2 + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$APP_NAME" ]; then + echo -e "\033[31mError: Application name is required\033[0m" + exit 1 +fi + +if [ -z "$APP_IDENTIFIER" ]; then + echo -e "\033[31mError: Application identifier is required\033[0m" + exit 1 +fi + +if [ -z "$COMPANY_NAME" ]; then + echo -e "\033[31mError: Company name is required\033[0m" + exit 1 +fi + +if [ -z "$COPYRIGHT" ]; then + echo -e "\033[31mError: Copyright information is required\033[0m" + exit 1 +fi + +if [ -z "$ICON_PATH" ]; then + echo -e "\033[31mError: Icon path is required\033[0m" + exit 1 +fi + +echo "Starting Windows application customization..." + +if sed --version >/dev/null 2>&1; then + SED_INPLACE="-i" +else + SED_INPLACE="-i ''" +fi + +update_runner_files() { + runner_dir="appflowy_flutter/windows/runner" + + if [ -f "$runner_dir/Runner.rc" ]; then + sed $SED_INPLACE "s/VALUE \"CompanyName\", .*$/VALUE \"CompanyName\", \"$COMPANY_NAME\"/" "$runner_dir/Runner.rc" + sed $SED_INPLACE "s/VALUE \"FileDescription\", .*$/VALUE \"FileDescription\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" + sed $SED_INPLACE "s/VALUE \"InternalName\", .*$/VALUE \"InternalName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" + sed $SED_INPLACE "s/VALUE \"OriginalFilename\", .*$/VALUE \"OriginalFilename\", \"$APP_NAME.exe\"/" "$runner_dir/Runner.rc" + sed $SED_INPLACE "s/VALUE \"LegalCopyright\", .*$/VALUE \"LegalCopyright\", \"$COPYRIGHT\"/" "$runner_dir/Runner.rc" + sed $SED_INPLACE "s/VALUE \"ProductName\", .*$/VALUE \"ProductName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" + echo -e "Runner.rc updated successfully" + else + echo -e "\033[31mRunner.rc file not found\033[0m" + fi +} + +update_icon() { + if [ ! -z "$ICON_PATH" ] && [ -f "$ICON_PATH" ]; then + app_icon_path="appflowy_flutter/windows/runner/resources/app_icon.ico" + cp "$ICON_PATH" "$app_icon_path" + echo -e "Application icon updated successfully" + else + echo -e "\033[31mApplication icon file not found\033[0m" + fi +} + +update_cmake_lists() { + cmake_file="appflowy_flutter/windows/CMakeLists.txt" + if [ -f "$cmake_file" ]; then + sed $SED_INPLACE "s/set(BINARY_NAME .*)$/set(BINARY_NAME \"$APP_NAME\")/" "$cmake_file" + echo -e "CMake configuration updated successfully" + else + echo -e "\033[31mCMake configuration file not found\033[0m" + fi +} + +update_main_cpp() { + main_cpp_file="appflowy_flutter/windows/runner/main.cpp" + if [ -f "$main_cpp_file" ]; then + sed $SED_INPLACE "s/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"AppFlowyMutex\");/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"${APP_NAME}Mutex\");/" "$main_cpp_file" + sed $SED_INPLACE "s/HWND handle = FindWindowA(NULL, \"AppFlowy\");/HWND handle = FindWindowA(NULL, \"$APP_NAME\");/" "$main_cpp_file" + sed $SED_INPLACE "s/if (window.SendAppLinkToInstance(L\"AppFlowy\")) {/if (window.SendAppLinkToInstance(L\"$APP_NAME\")) {/" "$main_cpp_file" + sed $SED_INPLACE "s/if (!window.Create(L\"AppFlowy\", origin, size)) {/if (!window.Create(L\"$APP_NAME\", origin, size)) {/" "$main_cpp_file" + echo -e "main.cpp updated successfully" + else + echo -e "\033[31mMain.cpp file not found\033[0m" + fi +} + +echo "Applying customizations..." +update_runner_files +update_icon +update_cmake_lists +update_main_cpp + +echo "Windows application customization completed successfully!" From bbc7b6d172c975c9d2516509b17fcf043ea26db3 Mon Sep 17 00:00:00 2001 From: Aniket Patil <63067583+Aniket404@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:37:21 -0700 Subject: [PATCH 285/384] feat(i18n): add initial Marathi (mr-IN) translations (#7667) * feat(i18n): Add initial Marathi (mr-IN) translations * fix(i18n): Force add mr-IN.json in assets folder for localization * fix(i18n): Sync mr-IN.json from assets to resources * fix(i18n): Final sync of full Marathi translation in assets * feat(i18n): Force add Marathi translations to ignored directory * feat(i18n): Complete Marathi translations --- .../assets/translations/mr-IN.json | 3210 +++++++++++++++++ .../lib/startup/tasks/app_widget.dart | 1 + .../packages/flowy_infra/lib/language.dart | 4 +- frontend/resources/translations/mr-IN.json | 3210 +++++++++++++++++ 4 files changed, 6424 insertions(+), 1 deletion(-) create mode 100644 frontend/appflowy_flutter/assets/translations/mr-IN.json create mode 100644 frontend/resources/translations/mr-IN.json diff --git a/frontend/appflowy_flutter/assets/translations/mr-IN.json b/frontend/appflowy_flutter/assets/translations/mr-IN.json new file mode 100644 index 0000000000..f86a1e0081 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/mr-IN.json @@ -0,0 +1,3210 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "मी", + "welcomeText": "@:appName मध्ये आ पले स्वागत आहे.", + "welcomeTo": "मध्ये आ पले स्वागत आ हे", + "githubStarText": "GitHub वर स्टार करा", + "subscribeNewsletterText": "वृत्तपत्राची सदस्यता घ्या", + "letsGoButtonText": "क्विक स्टार्ट", + "title": "Title", + "youCanAlso": "तुम्ही देखील", + "and": "आ णि", + "failedToOpenUrl": "URL उघडण्यात अयशस्वी: {}", + "blockActions": { + "addBelowTooltip": "खाली जोडण्यासाठी क्लिक करा", + "addAboveCmd": "Alt+click", + "addAboveMacCmd": "Option+click", + "addAboveTooltip": "वर जोडण्यासाठी", + "dragTooltip": "Drag to move", + "openMenuTooltip": "मेनू उघडण्यासाठी क्लिक करा" + }, + "signUp": { + "buttonText": "साइन अप", + "title": "साइन अप to @:appName", + "getStartedText": "सुरुवात करा", + "emptyPasswordError": "पासवर्ड रिकामा असू शकत नाही", + "repeatPasswordEmptyError": "Repeat पासवर्ड रिकामा असू शकत नाही", + "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", + "alreadyHaveAnAccount": "आधीच खाते आहे?", + "emailHint": "Email", + "passwordHint": "Password", + "repeatPasswordHint": "पासवर्ड पुन्हा लिहा", + "signUpWith": "यामध्ये साइन अप करा:" + }, + "signIn": { + "loginTitle": "@:appName मध्ये लॉगिन करा", + "loginButtonText": "लॉगिन", + "loginStartWithAnonymous": "अनामिक सत्रासह पुढे जा", + "continueAnonymousUser": "अनामिक सत्रासह पुढे जा", + "anonymous": "अनामिक", + "buttonText": "साइन इन", + "signingInText": "साइन इन होत आहे...", + "forgotPassword": "पासवर्ड विसरलात?", + "emailHint": "ईमेल", + "passwordHint": "पासवर्ड", + "dontHaveAnAccount": "तुमचं खाते नाही?", + "createAccount": "खाते तयार करा", + "repeatPasswordEmptyError": "पुन्हा पासवर्ड रिकामा असू शकत नाही", + "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", + "syncPromptMessage": "डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका", + "or": "किंवा", + "signInWithGoogle": "Google सह पुढे जा", + "signInWithGithub": "GitHub सह पुढे जा", + "signInWithDiscord": "Discord सह पुढे जा", + "signInWithApple": "Apple सह पुढे जा", + "continueAnotherWay": "इतर पर्यायांनी पुढे जा", + "signUpWithGoogle": "Google सह साइन अप करा", + "signUpWithGithub": "GitHub सह साइन अप करा", + "signUpWithDiscord": "Discord सह साइन अप करा", + "signInWith": "यासह पुढे जा:", + "signInWithEmail": "ईमेलसह पुढे जा", + "signInWithMagicLink": "पुढे जा", + "signUpWithMagicLink": "Magic Link सह साइन अप करा", + "pleaseInputYourEmail": "कृपया तुमचा ईमेल पत्ता टाका", + "settings": "सेटिंग्ज", + "magicLinkSent": "Magic Link पाठवण्यात आली आहे!", + "invalidEmail": "कृपया वैध ईमेल पत्ता टाका", + "alreadyHaveAnAccount": "आधीच खाते आहे?", + "logIn": "लॉगिन", + "generalError": "काहीतरी चुकलं. कृपया नंतर प्रयत्न करा", + "limitRateError": "सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता", + "magicLinkSentDescription": "तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल." + }, + "workspace": { + "chooseWorkspace": "तुमचे workspace निवडा", + "defaultName": "माझे Workspace", + "create": "नवीन workspace तयार करा", + "new": "नवीन workspace", + "importFromNotion": "Notion मधून आयात करा", + "learnMore": "अधिक जाणून घ्या", + "reset": "workspace रीसेट करा", + "renameWorkspace": "workspace चे नाव बदला", + "workspaceNameCannotBeEmpty": "workspace चे नाव रिकामे असू शकत नाही", + "resetWorkspacePrompt": "workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.", + "hint": "workspace", + "notFoundError": "workspace सापडले नाही", + "failedToLoad": "काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.", + "errorActions": { + "reportIssue": "समस्या नोंदवा", + "reportIssueOnGithub": "Github वर समस्या नोंदवा", + "exportLogFiles": "लॉग फाइल्स निर्यात करा", + "reachOut": "Discord वर संपर्क करा" + }, + "menuTitle": "कार्यक्षेत्रे", + "deleteWorkspaceHintText": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.", + "createSuccess": "कार्यक्षेत्र यशस्वीरित्या तयार झाले", + "createFailed": "कार्यक्षेत्र तयार करण्यात अयशस्वी", + "createLimitExceeded": "तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.", + "deleteSuccess": "कार्यक्षेत्र यशस्वीरित्या हटवले गेले", + "deleteFailed": "कार्यक्षेत्र हटवण्यात अयशस्वी", + "openSuccess": "कार्यक्षेत्र यशस्वीरित्या उघडले", + "openFailed": "कार्यक्षेत्र उघडण्यात अयशस्वी", + "renameSuccess": "कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले", + "renameFailed": "कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी", + "updateIconSuccess": "कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले", + "updateIconFailed": "कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी", + "cannotDeleteTheOnlyWorkspace": "फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही", + "fetchWorkspacesFailed": "कार्यक्षेत्रे मिळवण्यात अयशस्वी", + "leaveCurrentWorkspace": "कार्यक्षेत्र सोडा", + "leaveCurrentWorkspacePrompt": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?" + }, + "shareAction": { + "buttonText": "शेअर करा", + "workInProgress": "लवकरच येत आहे", + "markdown": "Markdown", + "html": "HTML", + "clipboard": "क्लिपबोर्डवर कॉपी करा", + "csv": "CSV", + "copyLink": "लिंक कॉपी करा", + "publishToTheWeb": "वेबवर प्रकाशित करा", + "publishToTheWebHint": "AppFlowy सह वेबसाइट तयार करा", + "publish": "प्रकाशित करा", + "unPublish": "अप्रकाशित करा", + "visitSite": "साइटला भेट द्या", + "exportAsTab": "या स्वरूपात निर्यात करा", + "publishTab": "प्रकाशित करा", + "shareTab": "शेअर करा", + "publishOnAppFlowy": "AppFlowy वर प्रकाशित करा", + "shareTabTitle": "सहकार्य करण्यासाठी आमंत्रित करा", + "shareTabDescription": "कोणासही सहज सहकार्य करण्यासाठी", + "copyLinkSuccess": "लिंक क्लिपबोर्डवर कॉपी केली", + "copyShareLink": "शेअर लिंक कॉपी करा", + "copyLinkFailed": "लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", + "copyLinkToBlockSuccess": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली", + "copyLinkToBlockFailed": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", + "manageAllSites": "सर्व साइट्स व्यवस्थापित करा", + "updatePathName": "पथाचे नाव अपडेट करा" + }, + "moreAction": { + "small": "लहान", + "medium": "मध्यम", + "large": "मोठा", + "fontSize": "फॉन्ट आकार", + "import": "Import", + "moreOptions": "अधिक पर्याय", + "wordCount": "शब्द संख्या: {}", + "charCount": "अक्षर संख्या: {}", + "createdAt": "निर्मिती: {}", + "deleteView": "हटवा", + "duplicateView": "प्रत बनवा", + "wordCountLabel": "शब्द संख्या: ", + "charCountLabel": "अक्षर संख्या: ", + "createdAtLabel": "निर्मिती: ", + "syncedAtLabel": "सिंक केले: ", + "saveAsNewPage": "संदेश पृष्ठात जोडा", + "saveAsNewPageDisabled": "उपलब्ध संदेश नाहीत" + }, + "importPanel": { + "textAndMarkdown": "मजकूर आणि Markdown", + "documentFromV010": "v0.1.0 पासून दस्तऐवज", + "databaseFromV010": "v0.1.0 पासून डेटाबेस", + "notionZip": "Notion निर्यात केलेली Zip फाईल", + "csv": "CSV", + "database": "डेटाबेस" + }, + "emojiIconPicker": { + "iconUploader": { + "placeholderLeft": "फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा ", + "placeholderUpload": "अपलोड", + "placeholderRight": ", किंवा इमेज लिंक पेस्ट करा.", + "dropToUpload": "अपलोडसाठी फाईल ड्रॉप करा", + "change": "बदला" + } + }, + "disclosureAction": { + "rename": "नाव बदला", + "delete": "हटवा", + "duplicate": "प्रत बनवा", + "unfavorite": "आवडतीतून काढा", + "favorite": "आवडतीत जोडा", + "openNewTab": "नवीन टॅबमध्ये उघडा", + "moveTo": "या ठिकाणी हलवा", + "addToFavorites": "आवडतीत जोडा", + "copyLink": "लिंक कॉपी करा", + "changeIcon": "आयकॉन बदला", + "collapseAllPages": "सर्व उपपृष्ठे संकुचित करा", + "movePageTo": "पृष्ठ हलवा", + "move": "हलवा", + "lockPage": "पृष्ठ लॉक करा" + }, + "blankPageTitle": "रिक्त पृष्ठ", + "newPageText": "नवीन पृष्ठ", + "newDocumentText": "नवीन दस्तऐवज", + "newGridText": "नवीन ग्रिड", + "newCalendarText": "नवीन कॅलेंडर", + "newBoardText": "नवीन बोर्ड", + "chat": { + "newChat": "AI गप्पा", + "inputMessageHint": "@:appName AI ला विचार करा", + "inputLocalAIMessageHint": "@:appName लोकल AI ला विचार करा", + "unsupportedCloudPrompt": "ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे", + "relatedQuestion": "सूचवलेले", + "serverUnavailable": "कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा", + "aiServerUnavailable": "AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.", + "retry": "पुन्हा प्रयत्न करा", + "clickToRetry": "पुन्हा प्रयत्न करण्यासाठी क्लिक करा", + "regenerateAnswer": "उत्तर पुन्हा तयार करा", + "question1": "Kanban वापरून कामे कशी व्यवस्थापित करायची", + "question2": "GTD पद्धत समजावून सांगा", + "question3": "Rust का वापरावा", + "question4": "माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी", + "question5": "माझ्या पृष्ठासाठी एक चित्र तयार करा", + "question6": "या आठवड्याची माझी कामांची यादी तयार करा", + "aiMistakePrompt": "AI चुकू शकतो. महत्त्वाची माहिती तपासा.", + "chatWithFilePrompt": "तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?", + "indexFileSuccess": "फाईल यशस्वीरित्या अनुक्रमित केली गेली", + "inputActionNoPages": "काहीही पृष्ठे सापडली नाहीत", + "referenceSource": { + "zero": "0 स्रोत सापडले", + "one": "{count} स्रोत सापडला", + "other": "{count} स्रोत सापडले" + } + }, + "clickToMention": "पृष्ठाचा उल्लेख करा", + "uploadFile": "PDFs, मजकूर किंवा markdown फाइल्स जोडा", + "questionDetail": "नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?", + "indexingFile": "{} अनुक्रमित करत आहे", + "generatingResponse": "उत्तर तयार होत आहे", + "selectSources": "स्रोत निवडा", + "currentPage": "सध्याचे पृष्ठ", + "sourcesLimitReached": "तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता", + "sourceUnsupported": "सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही", + "regenerate": "पुन्हा प्रयत्न करा", + "addToPageButton": "संदेश पृष्ठावर जोडा", + "addToPageTitle": "या पृष्ठात संदेश जोडा...", + "addToNewPage": "नवीन पृष्ठ तयार करा", + "addToNewPageName": "\"{}\" मधून काढलेले संदेश", + "addToNewPageSuccessToast": "संदेश जोडण्यात आला", + "openPagePreviewFailedToast": "पृष्ठ उघडण्यात अयशस्वी", + "changeFormat": { + "actionButton": "फॉरमॅट बदला", + "confirmButton": "या फॉरमॅटसह पुन्हा तयार करा", + "textOnly": "मजकूर", + "imageOnly": "फक्त प्रतिमा", + "textAndImage": "मजकूर आणि प्रतिमा", + "text": "परिच्छेद", + "bullet": "बुलेट यादी", + "number": "क्रमांकित यादी", + "table": "सारणी", + "blankDescription": "उत्तराचे फॉरमॅट ठरवा", + "defaultDescription": "स्वयंचलित उत्तर फॉरमॅट", + "textWithImageDescription": "@:chat.changeFormat.text प्रतिमेसह", + "numberWithImageDescription": "@:chat.changeFormat.number प्रतिमेसह", + "bulletWithImageDescription": "@:chat.changeFormat.bullet प्रतिमेसह", + " tableWithImageDescription": "@:chat.changeFormat.table प्रतिमेसह" + }, + "switchModel": { + "label": "मॉडेल बदला", + "localModel": "स्थानिक मॉडेल", + "cloudModel": "क्लाऊड मॉडेल", + "autoModel": "स्वयंचलित" + }, + "selectBanner": { + "saveButton": "… मध्ये जोडा", + "selectMessages": "संदेश निवडा", + "nSelected": "{} निवडले गेले", + "allSelected": "सर्व निवडले गेले" + }, + "stopTooltip": "उत्पन्न करणे थांबवा", + "trash": { + "text": "कचरा", + "restoreAll": "सर्व पुनर्संचयित करा", + "restore": "पुनर्संचयित करा", + "deleteAll": "सर्व हटवा", + "pageHeader": { + "fileName": "फाईलचे नाव", + "lastModified": "शेवटचा बदल", + "created": "निर्मिती" + } + }, + "confirmDeleteAll": { + "title": "कचरापेटीतील सर्व पृष्ठे", + "caption": "तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "confirmRestoreAll": { + "title": "कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा", + "caption": "ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "restorePage": { + "title": "पुनर्संचयित करा: {}", + "caption": "तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?" + }, + "mobile": { + "actions": "कचरा क्रिया", + "empty": "कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत", + "emptyDescription": "जे आवश्यक नाही ते कचरापेटीत हलवा.", + "isDeleted": "हटवले गेले आहे", + "isRestored": "पुनर्संचयित केले गेले आहे" + }, + "confirmDeleteTitle": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?", + "deletePagePrompt": { + "text": "हे पृष्ठ कचरापेटीत आहे", + "restore": "पृष्ठ पुनर्संचयित करा", + "deletePermanent": "कायमचे हटवा", + "deletePermanentDescription": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "dialogCreatePageNameHint": "पृष्ठाचे नाव", + "questionBubble": { + "shortcuts": "शॉर्टकट्स", + "whatsNew": "नवीन काय आहे?", + "help": "मदत आणि समर्थन", + "markdown": "Markdown", + "debug": { + "name": "डीबग माहिती", + "success": "डीबग माहिती क्लिपबोर्डवर कॉपी केली!", + "fail": "डीबग माहिती कॉपी करता आली नाही" + }, + "feedback": "अभिप्राय" + }, + "menuAppHeader": { + "moreButtonToolTip": "हटवा, नाव बदला आणि अधिक...", + "addPageTooltip": "तत्काळ एक पृष्ठ जोडा", + "defaultNewPageName": "शीर्षक नसलेले", + "renameDialog": "नाव बदला", + "pageNameSuffix": "प्रत" + }, + "noPagesInside": "अंदर कोणतीही पृष्ठे नाहीत", + "toolbar": { + "undo": "पूर्ववत करा", + "redo": "पुन्हा करा", + "bold": "ठळक", + "italic": "तिरकस", + "underline": "अधोरेखित", + "strike": "मागे ओढलेले", + "numList": "क्रमांकित यादी", + "bulletList": "बुलेट यादी", + "checkList": "चेक यादी", + "inlineCode": "इनलाइन कोड", + "quote": "उद्धरण ब्लॉक", + "header": "शीर्षक", + "highlight": "हायलाइट", + "color": "रंग", + "addLink": "लिंक जोडा" + }, + "tooltip": { + "lightMode": "लाइट मोडमध्ये स्विच करा", + "darkMode": "डार्क मोडमध्ये स्विच करा", + "openAsPage": "पृष्ठ म्हणून उघडा", + "addNewRow": "नवीन पंक्ती जोडा", + "openMenu": "मेनू उघडण्यासाठी क्लिक करा", + "dragRow": "पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा", + "viewDataBase": "डेटाबेस पहा", + "referencePage": "हे {name} संदर्भित आहे", + "addBlockBelow": "खाली एक ब्लॉक जोडा", + "aiGenerate": "निर्मिती करा" + }, + "sideBar": { + "closeSidebar": "साइडबार बंद करा", + "openSidebar": "साइडबार उघडा", + "expandSidebar": "पूर्ण पृष्ठावर विस्तारित करा", + "personal": "वैयक्तिक", + "private": "खाजगी", + "workspace": "कार्यक्षेत्र", + "favorites": "आवडती", + "clickToHidePrivate": "खाजगी जागा लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील", + "clickToHideWorkspace": "कार्यक्षेत्र लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील", + "clickToHidePersonal": "वैयक्तिक जागा लपवण्यासाठी क्लिक करा", + "clickToHideFavorites": "आवडती जागा लपवण्यासाठी क्लिक करा", + "addAPage": "नवीन पृष्ठ जोडा", + "addAPageToPrivate": "खाजगी जागेत पृष्ठ जोडा", + "addAPageToWorkspace": "कार्यक्षेत्रात पृष्ठ जोडा", + "recent": "अलीकडील", + "today": "आज", + "thisWeek": "या आठवड्यात", + "others": "पूर्वीच्या आवडती", + "earlier": "पूर्वीचे", + "justNow": "आत्ताच", + "minutesAgo": "{count} मिनिटांपूर्वी", + "lastViewed": "शेवटी पाहिलेले", + "favoriteAt": "आवडते म्हणून चिन्हांकित", + "emptyRecent": "अलीकडील पृष्ठे नाहीत", + "emptyRecentDescription": "तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.", + "emptyFavorite": "आवडती पृष्ठे नाहीत", + "emptyFavoriteDescription": "पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!", + "removePageFromRecent": "हे पृष्ठ अलीकडील यादीतून काढायचे?", + "removeSuccess": "यशस्वीरित्या काढले गेले", + "favoriteSpace": "आवडती", + "RecentSpace": "अलीकडील", + "Spaces": "जागा", + "upgradeToPro": "Pro मध्ये अपग्रेड करा", + "upgradeToAIMax": "अमर्यादित AI अनलॉक करा", + "storageLimitDialogTitle": "तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा", + "storageLimitDialogTitleIOS": "तुमचा मोफत स्टोरेज संपला आहे.", + "aiResponseLimitTitle": "तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा", + "aiResponseLimitDialogTitle": "AI प्रतिसाद मर्यादा गाठली आहे", + "aiResponseLimit": "तुमचे मोफत AI प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा", + "askOwnerToUpgradeToPro": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा", + "askOwnerToUpgradeToProIOS": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.", + "askOwnerToUpgradeToAIMax": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा", + "askOwnerToUpgradeToAIMaxIOS": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.", + "purchaseAIMax": "तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा", + "aiImageResponseLimit": "तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा", + "purchaseStorageSpace": "स्टोरेज स्पेस खरेदी करा", + "singleFileProPlanLimitationDescription": "तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा", + "purchaseAIResponse": "AI प्रतिसाद खरेदी करा", + "askOwnerToUpgradeToLocalAI": "कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा", + "upgradeToAILocal": "अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा", + "upgradeToAILocalDesc": "PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा" +}, + "notifications": { + "export": { + "markdown": "टीप Markdown मध्ये निर्यात केली", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "संपर्क", + "whatsHappening": "या आठवड्यात काय घडत आहे?", + "addContact": "संपर्क जोडा", + "editContact": "संपर्क संपादित करा" + }, + "button": { + "ok": "ठीक आहे", + "confirm": "खात्री करा", + "done": "पूर्ण", + "cancel": "रद्द करा", + "signIn": "साइन इन", + "signOut": "साइन आउट", + "complete": "पूर्ण करा", + "save": "जतन करा", + "generate": "निर्माण करा", + "esc": "ESC", + "keep": "ठेवा", + "tryAgain": "पुन्हा प्रयत्न करा", + "discard": "टाका", + "replace": "बदला", + "insertBelow": "खाली घाला", + "insertAbove": "वर घाला", + "upload": "अपलोड करा", + "edit": "संपादित करा", + "delete": "हटवा", + "copy": "कॉपी करा", + "duplicate": "प्रत बनवा", + "putback": "परत ठेवा", + "update": "अद्यतनित करा", + "share": "शेअर करा", + "removeFromFavorites": "आवडतीतून काढा", + "removeFromRecent": "अलीकडील यादीतून काढा", + "addToFavorites": "आवडतीत जोडा", + "favoriteSuccessfully": "आवडतीत यशस्वीरित्या जोडले", + "unfavoriteSuccessfully": "आवडतीतून यशस्वीरित्या काढले", + "duplicateSuccessfully": "प्रत यशस्वीरित्या तयार झाली", + "rename": "नाव बदला", + "helpCenter": "मदत केंद्र", + "add": "जोड़ा", + "yes": "होय", + "no": "नाही", + "clear": "साफ करा", + "remove": "काढा", + "dontRemove": "काढू नका", + "copyLink": "लिंक कॉपी करा", + "align": "जुळवा", + "login": "लॉगिन", + "logout": "लॉगआउट", + "deleteAccount": "खाते हटवा", + "back": "मागे", + "signInGoogle": "Google सह पुढे जा", + "signInGithub": "GitHub सह पुढे जा", + "signInDiscord": "Discord सह पुढे जा", + "more": "अधिक", + "create": "तयार करा", + "close": "बंद करा", + "next": "पुढे", + "previous": "मागील", + "submit": "सबमिट करा", + "download": "डाउनलोड करा", + "backToHome": "मुख्यपृष्ठावर परत जा", + "viewing": "पाहत आहात", + "editing": "संपादन करत आहात", + "gotIt": "समजले", + "retry": "पुन्हा प्रयत्न करा", + "uploadFailed": "अपलोड अयशस्वी.", + "copyLinkOriginal": "मूळ दुव्याची कॉपी करा" + }, + "label": { + "welcome": "स्वागत आहे!", + "firstName": "पहिले नाव", + "middleName": "मधले नाव", + "lastName": "आडनाव", + "stepX": "पायरी {X}" + }, + "oAuth": { + "err": { + "failedTitle": "तुमच्या खात्याशी कनेक्ट होता आले नाही.", + "failedMsg": "कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे." + }, + "google": { + "title": "GOOGLE साइन-इन", + "instruction1": "तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.", + "instruction2": "ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:", + "instruction3": "तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:", + "instruction4": "साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:" + } + }, + "settings": { + "title": "सेटिंग्ज", + "popupMenuItem": { + "settings": "सेटिंग्ज", + "members": "सदस्य", + "trash": "कचरा", + "helpAndSupport": "मदत आणि समर्थन" + }, + "sites": { + "title": "साइट्स", + "namespaceTitle": "नेमस्पेस", + "namespaceDescription": "तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा", + "namespaceHeader": "नेमस्पेस", + "homepageHeader": "मुख्यपृष्ठ", + "updateNamespace": "नेमस्पेस अद्यतनित करा", + "removeHomepage": "मुख्यपृष्ठ हटवा", + "selectHomePage": "एक पृष्ठ निवडा", + "clearHomePage": "या नेमस्पेससाठी मुख्यपृष्ठ साफ करा", + "customUrl": "स्वतःची URL", + "namespace": { + "description": "हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत", + "tooltip": "कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो", + "updateExistingNamespace": "विद्यमान नेमस्पेस अद्यतनित करा", + "upgradeToPro": "मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा", + "redirectToPayment": "पेमेंट पृष्ठावर वळवत आहे...", + "onlyWorkspaceOwnerCanSetHomePage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो", + "pleaseAskOwnerToSetHomePage": "कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा" + }, + "publishedPage": { + "title": "सर्व प्रकाशित पृष्ठे", + "description": "तुमची प्रकाशित पृष्ठे व्यवस्थापित करा", + "page": "पृष्ठ", + "pathName": "पथाचे नाव", + "date": "प्रकाशन तारीख", + "emptyHinText": "या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत", + "noPublishedPages": "प्रकाशित पृष्ठे नाहीत", + "settings": "प्रकाशन सेटिंग्ज", + "clickToOpenPageInApp": "पृष्ठ अ‍ॅपमध्ये उघडा", + "clickToOpenPageInBrowser": "पृष्ठ ब्राउझरमध्ये उघडा" + } + } + }, + "error": { + "failedToGeneratePaymentLink": "Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी", + "failedToUpdateNamespace": "नेमस्पेस अद्यतनित करण्यात अयशस्वी", + "proPlanLimitation": "नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे", + "namespaceAlreadyInUse": "नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा", + "invalidNamespace": "अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा", + "namespaceLengthAtLeast2Characters": "नेमस्पेस किमान २ अक्षरे लांब असावे", + "onlyWorkspaceOwnerCanUpdateNamespace": "फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो", + "onlyWorkspaceOwnerCanRemoveHomepage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो", + "setHomepageFailed": "मुख्यपृष्ठ सेट करण्यात अयशस्वी", + "namespaceTooLong": "नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा", + "namespaceTooShort": "नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा", + "namespaceIsReserved": "हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा", + "updatePathNameFailed": "पथाचे नाव अद्यतनित करण्यात अयशस्वी", + "removeHomePageFailed": "मुख्यपृष्ठ हटवण्यात अयशस्वी", + "publishNameContainsInvalidCharacters": "पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", + "publishNameTooShort": "पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा", + "publishNameTooLong": "पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा", + "publishNameAlreadyInUse": "हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा", + "namespaceContainsInvalidCharacters": "नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", + "publishPermissionDenied": "फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो", + "publishNameCannotBeEmpty": "पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा" + }, + "success": { + "namespaceUpdated": "नेमस्पेस यशस्वीरित्या अद्यतनित केला", + "setHomepageSuccess": "मुख्यपृष्ठ यशस्वीरित्या सेट केले", + "updatePathNameSuccess": "पथाचे नाव यशस्वीरित्या अद्यतनित केले", + "removeHomePageSuccess": "मुख्यपृष्ठ यशस्वीरित्या हटवले" + }, + "accountPage": { + "menuLabel": "खाते आणि अ‍ॅप", + "title": "माझे खाते", + "general": { + "title": "खात्याचे नाव आणि प्रोफाइल प्रतिमा", + "changeProfilePicture": "प्रोफाइल प्रतिमा बदला" + }, + "email": { + "title": "ईमेल", + "actions": { + "change": "ईमेल बदला" + } + }, + "login": { + "title": "खाते लॉगिन", + "loginLabel": "लॉगिन", + "logoutLabel": "लॉगआउट" + }, + "isUpToDate": "@:appName अद्ययावत आहे!", + "officialVersion": "आवृत्ती {version} (अधिकृत बिल्ड)" +}, + "workspacePage": { + "menuLabel": "कार्यक्षेत्र", + "title": "कार्यक्षेत्र", + "description": "तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.", + "workspaceName": { + "title": "कार्यक्षेत्राचे नाव" + }, + "workspaceIcon": { + "title": "कार्यक्षेत्राचे चिन्ह", + "description": "तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल." + }, + "appearance": { + "title": "दृश्यरूप", + "description": "कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.", + "options": { + "system": "स्वयंचलित", + "light": "लाइट", + "dark": "डार्क" + } + } + }, + "resetCursorColor": { + "title": "दस्तऐवज कर्सरचा रंग रीसेट करा", + "description": "तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?" + }, + "resetSelectionColor": { + "title": "दस्तऐवज निवडीचा रंग रीसेट करा", + "description": "तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?" + }, + "resetWidth": { + "resetSuccess": "दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली" + }, + "theme": { + "title": "थीम", + "description": "पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.", + "uploadCustomThemeTooltip": "स्वतःची थीम अपलोड करा" + }, + "workspaceFont": { + "title": "कार्यक्षेत्र फॉन्ट", + "noFontHint": "कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा." + }, + "textDirection": { + "title": "मजकूर दिशा", + "leftToRight": "डावीकडून उजवीकडे", + "rightToLeft": "उजवीकडून डावीकडे", + "auto": "स्वयंचलित", + "enableRTLItems": "RTL टूलबार घटक सक्षम करा" + }, + "layoutDirection": { + "title": "लेआउट दिशा", + "leftToRight": "डावीकडून उजवीकडे", + "rightToLeft": "उजवीकडून डावीकडे" + }, + "dateTime": { + "title": "दिनांक आणि वेळ", + "example": "{} वाजता {} ({})", + "24HourTime": "२४-तास वेळ", + "dateFormat": { + "label": "दिनांक फॉरमॅट", + "local": "स्थानिक", + "us": "US", + "iso": "ISO", + "friendly": "सुलभ", + "dmy": "D/M/Y" + } + }, + "language": { + "title": "भाषा" + }, + "deleteWorkspacePrompt": { + "title": "कार्यक्षेत्र हटवा", + "content": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील." + }, + "leaveWorkspacePrompt": { + "title": "कार्यक्षेत्र सोडा", + "content": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.", + "success": "तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.", + "fail": "कार्यक्षेत्र सोडण्यात अयशस्वी." + }, + "manageWorkspace": { + "title": "कार्यक्षेत्र व्यवस्थापित करा", + "leaveWorkspace": "कार्यक्षेत्र सोडा", + "deleteWorkspace": "कार्यक्षेत्र हटवा" + }, + "manageDataPage": { + "menuLabel": "डेटा व्यवस्थापित करा", + "title": "डेटा व्यवस्थापन", + "description": "@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.", + "dataStorage": { + "title": "फाइल संचयन स्थान", + "tooltip": "जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान", + "actions": { + "change": "मार्ग बदला", + "open": "फोल्डर उघडा", + "openTooltip": "सध्याच्या डेटा फोल्डरचे स्थान उघडा", + "copy": "मार्ग कॉपी करा", + "copiedHint": "मार्ग कॉपी केला!", + "resetTooltip": "मूलभूत स्थानावर रीसेट करा" + }, + "resetDialog": { + "title": "तुम्हाला खात्री आहे का?", + "description": "पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा." + } + }, + "importData": { + "title": "डेटा आयात करा", + "tooltip": "@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा", + "description": "बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा", + "action": "फाइल निवडा" + }, + "encryption": { + "title": "एनक्रिप्शन", + "tooltip": "तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा", + "descriptionNoEncryption": "एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.", + "descriptionEncrypted": "तुमचा डेटा एनक्रिप्टेड आहे.", + "action": "डेटा एनक्रिप्ट करा", + "dialog": { + "title": "संपूर्ण डेटा एनक्रिप्ट करायचा?", + "description": "तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?" + } + }, + "cache": { + "title": "कॅशे साफ करा", + "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", + "dialog": { + "title": "कॅशे साफ करा", + "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", + "successHint": "कॅशे साफ झाली!" + } + }, + "data": { + "fixYourData": "तुमचा डेटा सुधारा", + "fixButton": "सुधारा", + "fixYourDataDescription": "तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता." + } + }, + "shortcutsPage": { + "menuLabel": "शॉर्टकट्स", + "title": "शॉर्टकट्स", + "editBindingHint": "नवीन बाइंडिंग टाका", + "searchHint": "शोधा", + "actions": { + "resetDefault": "मूलभूत रीसेट करा" + }, + "errorPage": { + "message": "शॉर्टकट्स लोड करण्यात अयशस्वी: {}", + "howToFix": "कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा." + }, + "resetDialog": { + "title": "शॉर्टकट्स रीसेट करा", + "description": "हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?", + "buttonLabel": "रीसेट करा" + }, + "conflictDialog": { + "title": "{} आधीच वापरले जात आहे", + "descriptionPrefix": "हे कीबाइंडिंग सध्या ", + "descriptionSuffix": " यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.", + "confirmLabel": "पुढे जा" + }, + "editTooltip": "कीबाइंडिंग संपादित करण्यासाठी क्लिक करा", + "keybindings": { + "toggleToDoList": "टू-डू सूची चालू/बंद करा", + "insertNewParagraphInCodeblock": "नवीन परिच्छेद टाका", + "pasteInCodeblock": "कोडब्लॉकमध्ये पेस्ट करा", + "selectAllCodeblock": "सर्व निवडा", + "indentLineCodeblock": "ओळीच्या सुरुवातीला दोन स्पेस टाका", + "outdentLineCodeblock": "ओळीच्या सुरुवातीची दोन स्पेस काढा", + "twoSpacesCursorCodeblock": "कर्सरवर दोन स्पेस टाका", + "copy": "निवड कॉपी करा", + "paste": "मजकुरात पेस्ट करा", + "cut": "निवड कट करा", + "alignLeft": "मजकूर डावीकडे संरेखित करा", + "alignCenter": "मजकूर मधोमध संरेखित करा", + "alignRight": "मजकूर उजवीकडे संरेखित करा", + "insertInlineMathEquation": "इनलाइन गणितीय सूत्र टाका", + "undo": "पूर्ववत करा", + "redo": "पुन्हा करा", + "convertToParagraph": "ब्लॉक परिच्छेदात रूपांतरित करा", + "backspace": "हटवा", + "deleteLeftWord": "डावीकडील शब्द हटवा", + "deleteLeftSentence": "डावीकडील वाक्य हटवा", + "delete": "उजवीकडील अक्षर हटवा", + "deleteMacOS": "डावीकडील अक्षर हटवा", + "deleteRightWord": "उजवीकडील शब्द हटवा", + "moveCursorLeft": "कर्सर डावीकडे हलवा", + "moveCursorBeginning": "कर्सर सुरुवातीला हलवा", + "moveCursorLeftWord": "कर्सर एक शब्द डावीकडे हलवा", + "moveCursorLeftSelect": "निवडा आणि कर्सर डावीकडे हलवा", + "moveCursorBeginSelect": "निवडा आणि कर्सर सुरुवातीला हलवा", + "moveCursorLeftWordSelect": "निवडा आणि कर्सर एक शब्द डावीकडे हलवा", + "moveCursorRight": "कर्सर उजवीकडे हलवा", + "moveCursorEnd": "कर्सर शेवटी हलवा", + "moveCursorRightWord": "कर्सर एक शब्द उजवीकडे हलवा", + "moveCursorRightSelect": "निवडा आणि कर्सर उजवीकडे हलवा", + "moveCursorEndSelect": "निवडा आणि कर्सर शेवटी हलवा", + "moveCursorRightWordSelect": "निवडा आणि कर्सर एक शब्द उजवीकडे हलवा", + "moveCursorUp": "कर्सर वर हलवा", + "moveCursorTopSelect": "निवडा आणि कर्सर वर हलवा", + "moveCursorTop": "कर्सर वर हलवा", + "moveCursorUpSelect": "निवडा आणि कर्सर वर हलवा", + "moveCursorBottomSelect": "निवडा आणि कर्सर खाली हलवा", + "moveCursorBottom": "कर्सर खाली हलवा", + "moveCursorDown": "कर्सर खाली हलवा", + "moveCursorDownSelect": "निवडा आणि कर्सर खाली हलवा", + "home": "वर स्क्रोल करा", + "end": "खाली स्क्रोल करा", + "toggleBold": "बोल्ड चालू/बंद करा", + "toggleItalic": "इटालिक चालू/बंद करा", + "toggleUnderline": "अधोरेखित चालू/बंद करा", + "toggleStrikethrough": "स्ट्राईकथ्रू चालू/बंद करा", + "toggleCode": "इनलाइन कोड चालू/बंद करा", + "toggleHighlight": "हायलाईट चालू/बंद करा", + "showLinkMenu": "लिंक मेनू दाखवा", + "openInlineLink": "इनलाइन लिंक उघडा", + "openLinks": "सर्व निवडलेले लिंक उघडा", + "indent": "इंडेंट", + "outdent": "आउटडेंट", + "exit": "संपादनातून बाहेर पडा", + "pageUp": "एक पृष्ठ वर स्क्रोल करा", + "pageDown": "एक पृष्ठ खाली स्क्रोल करा", + "selectAll": "सर्व निवडा", + "pasteWithoutFormatting": "फॉरमॅटिंगशिवाय पेस्ट करा", + "showEmojiPicker": "इमोजी निवडकर्ता दाखवा", + "enterInTableCell": "टेबलमध्ये नवीन ओळ जोडा", + "leftInTableCell": "टेबलमध्ये डावीकडील सेलमध्ये जा", + "rightInTableCell": "टेबलमध्ये उजवीकडील सेलमध्ये जा", + "upInTableCell": "टेबलमध्ये वरील सेलमध्ये जा", + "downInTableCell": "टेबलमध्ये खालील सेलमध्ये जा", + "tabInTableCell": "टेबलमध्ये पुढील सेलमध्ये जा", + "shiftTabInTableCell": "टेबलमध्ये मागील सेलमध्ये जा", + "backSpaceInTableCell": "सेलच्या सुरुवातीला थांबा" + }, + "commands": { + "codeBlockNewParagraph": "कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका", + "codeBlockIndentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका", + "codeBlockOutdentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा", + "codeBlockAddTwoSpaces": "कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका", + "codeBlockSelectAll": "कोडब्लॉकमधील सर्व मजकूर निवडा", + "codeBlockPasteText": "कोडब्लॉकमध्ये मजकूर पेस्ट करा", + "textAlignLeft": "मजकूर डावीकडे संरेखित करा", + "textAlignCenter": "मजकूर मधोमध संरेखित करा", + "textAlignRight": "मजकूर उजवीकडे संरेखित करा" + }, + "couldNotLoadErrorMsg": "शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा", + "couldNotSaveErrorMsg": "शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा" +}, + "aiPage": { + "title": "AI सेटिंग्ज", + "menuLabel": "AI सेटिंग्ज", + "keys": { + "enableAISearchTitle": "AI शोध", + "aiSettingsDescription": "AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.", + "loginToEnableAIFeature": "AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.", + "llmModel": "भाषा मॉडेल", + "llmModelType": "भाषा मॉडेल प्रकार", + "downloadLLMPrompt": "{} डाउनलोड करा", + "downloadAppFlowyOfflineAI": "AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?", + "downloadLLMPromptDetail": "{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?", + "downloadBigFilePrompt": "डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात", + "downloadAIModelButton": "डाउनलोड करा", + "downloadingModel": "डाउनलोड करत आहे", + "localAILoaded": "स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे", + "localAIStart": "स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा", + "localAILoading": "स्थानिक AI Chat मॉडेल लोड होत आहे...", + "localAIStopped": "स्थानिक AI थांबले आहे", + "localAIRunning": "स्थानिक AI चालू आहे", + "localAINotReadyRetryLater": "स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा", + "localAIDisabled": "तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा", + "localAIInitializing": "स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात", + "localAINotReadyTextFieldPrompt": "स्थानिक AI लोड होत असताना संपादन करता येणार नाही", + "failToLoadLocalAI": "स्थानिक AI सुरू करण्यात अयशस्वी.", + "restartLocalAI": "पुन्हा सुरू करा", + "disableLocalAITitle": "स्थानिक AI अकार्यक्षम करा", + "disableLocalAIDescription": "तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?", + "localAIToggleTitle": "AppFlowy स्थानिक AI (LAI)", + "localAIToggleSubTitle": "AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा", + "offlineAIInstruction1": "हे अनुसरा", + "offlineAIInstruction2": "सूचना", + "offlineAIInstruction3": "ऑफलाइन AI सक्षम करण्यासाठी.", + "offlineAIDownload1": "जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया", + "offlineAIDownload2": "डाउनलोड", + "offlineAIDownload3": "करा", + "activeOfflineAI": "सक्रिय", + "downloadOfflineAI": "डाउनलोड करा", + "openModelDirectory": "फोल्डर उघडा", + "laiNotReady": "स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.", + "ollamaNotReady": "Ollama सर्व्हर तयार नाही.", + "pleaseFollowThese": "कृपया हे अनुसरा", + "instructions": "सूचना", + "installOllamaLai": "Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.", + "modelsMissing": "आवश्यक मॉडेल्स सापडत नाहीत.", + "downloadModel": "त्यांना डाउनलोड करण्यासाठी." + } +}, + "planPage": { + "menuLabel": "योजना", + "title": "दर योजना", + "planUsage": { + "title": "योजनेचा वापर सारांश", + "storageLabel": "स्टोरेज", + "storageUsage": "{} पैकी {} GB", + "unlimitedStorageLabel": "अमर्यादित स्टोरेज", + "collaboratorsLabel": "सदस्य", + "collaboratorsUsage": "{} पैकी {}", + "aiResponseLabel": "AI प्रतिसाद", + "aiResponseUsage": "{} पैकी {}", + "unlimitedAILabel": "अमर्यादित AI प्रतिसाद", + "proBadge": "प्रो", + "aiMaxBadge": "AI Max", + "aiOnDeviceBadge": "मॅकसाठी ऑन-डिव्हाइस AI", + "memberProToggle": "अधिक सदस्य आणि अमर्यादित AI", + "aiMaxToggle": "अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश", + "aiOnDeviceToggle": "जास्त गोपनीयतेसाठी स्थानिक AI", + "aiCredit": { + "title": "@:appName AI क्रेडिट जोडा", + "price": "{}", + "priceDescription": "1,000 क्रेडिट्ससाठी", + "purchase": "AI खरेदी करा", + "info": "प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:", + "infoItemOne": "प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद", + "infoItemTwo": "प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद" + }, + "currentPlan": { + "bannerLabel": "सद्य योजना", + "freeTitle": "फ्री", + "proTitle": "प्रो", + "teamTitle": "टीम", + "freeInfo": "2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम", + "proInfo": "10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य", + "teamInfo": "सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य", + "upgrade": "योजना बदला", + "canceledInfo": "तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल." + }, + "addons": { + "title": "ऍड-ऑन्स", + "addLabel": "जोडा", + "activeLabel": "जोडले गेले", + "aiMax": { + "title": "AI Max", + "description": "प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा", + "price": "{}", + "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)" + }, + "aiOnDevice": { + "title": "मॅकसाठी ऑन-डिव्हाइस AI", + "description": "तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा", + "price": "{}", + "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)", + "recommend": "M1 किंवा नवीनतम शिफारस केली जाते" + } + }, + "deal": { + "bannerLabel": "नववर्षाचे विशेष ऑफर!", + "title": "तुमची टीम वाढवा!", + "info": "Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.", + "viewPlans": "योजना पहा" + } + } +}, + "billingPage": { + "menuLabel": "बिलिंग", + "title": "बिलिंग", + "plan": { + "title": "योजना", + "freeLabel": "फ्री", + "proLabel": "प्रो", + "planButtonLabel": "योजना बदला", + "billingPeriod": "बिलिंग कालावधी", + "periodButtonLabel": "कालावधी संपादित करा" + }, + "paymentDetails": { + "title": "पेमेंट तपशील", + "methodLabel": "पेमेंट पद्धत", + "methodButtonLabel": "पद्धत संपादित करा" + }, + "addons": { + "title": "ऍड-ऑन्स", + "addLabel": "जोडा", + "removeLabel": "काढा", + "renewLabel": "नवीन करा", + "aiMax": { + "label": "AI Max", + "description": "अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा", + "activeDescription": "पुढील बिलिंग तारीख {} आहे", + "canceledDescription": "AI Max {} पर्यंत उपलब्ध असेल" + }, + "aiOnDevice": { + "label": "मॅकसाठी ऑन-डिव्हाइस AI", + "description": "तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा", + "activeDescription": "पुढील बिलिंग तारीख {} आहे", + "canceledDescription": "मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल" + }, + "removeDialog": { + "title": "{} काढा", + "description": "तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल." + } + }, + "currentPeriodBadge": "सद्य कालावधी", + "changePeriod": "कालावधी बदला", + "planPeriod": "{} कालावधी", + "monthlyInterval": "मासिक", + "monthlyPriceInfo": "प्रति सदस्य, मासिक बिलिंग", + "annualInterval": "वार्षिक", + "annualPriceInfo": "प्रति सदस्य, वार्षिक बिलिंग" +}, + "comparePlanDialog": { + "title": "योजना तुलना आणि निवड", + "planFeatures": "योजनेची\nवैशिष्ट्ये", + "current": "सध्याची", + "actions": { + "upgrade": "अपग्रेड करा", + "downgrade": "डाऊनग्रेड करा", + "current": "सध्याची" + }, + "freePlan": { + "title": "फ्री", + "description": "२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी", + "price": "{}", + "priceInfo": "सदैव फ्री" + }, + "proPlan": { + "title": "प्रो", + "description": "लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी", + "price": "{}", + "priceInfo": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग\n\n{} मासिक बिलिंगसाठी" + }, + "planLabels": { + "itemOne": "वर्कस्पेसेस", + "itemTwo": "सदस्य", + "itemThree": "स्टोरेज", + "itemFour": "रिअल-टाइम सहकार्य", + "itemFive": "मोबाईल अ‍ॅप", + "itemSix": "AI प्रतिसाद", + "itemSeven": "AI प्रतिमा", + "itemFileUpload": "फाइल अपलोड", + "customNamespace": "सानुकूल नेमस्पेस", + "tooltipSix": "‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही", + "intelligentSearch": "स्मार्ट शोध", + "tooltipSeven": "तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते", + "customNamespaceTooltip": "सानुकूल प्रकाशित साइट URL" + }, + "freeLabels": { + "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", + "itemTwo": "२ पर्यंत", + "itemThree": "५ GB", + "itemFour": "होय", + "itemFive": "होय", + "itemSix": "१० कायमस्वरूपी", + "itemSeven": "२ कायमस्वरूपी", + "itemFileUpload": "७ MB पर्यंत", + "intelligentSearch": "स्मार्ट शोध" + }, + "proLabels": { + "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", + "itemTwo": "१० पर्यंत", + "itemThree": "अमर्यादित", + "itemFour": "होय", + "itemFive": "होय", + "itemSix": "अमर्यादित", + "itemSeven": "दर महिन्याला १० प्रतिमा", + "itemFileUpload": "अमर्यादित", + "intelligentSearch": "स्मार्ट शोध" + }, + "paymentSuccess": { + "title": "तुम्ही आता {} योजनेवर आहात!", + "description": "तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता." + }, + "downgradeDialog": { + "title": "तुम्हाला योजना डाऊनग्रेड करायची आहे का?", + "description": "तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.", + "downgradeLabel": "योजना डाऊनग्रेड करा" + } +}, + "cancelSurveyDialog": { + "title": "तुम्ही जात आहात याचे दुःख आहे", + "description": "तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.", + "commonOther": "इतर", + "otherHint": "तुमचे उत्तर येथे लिहा", + "questionOne": { + "question": "तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?", + "answerOne": "खर्च खूप जास्त आहे", + "answerTwo": "वैशिष्ट्ये अपेक्षांनुसार नव्हती", + "answerThree": "यापेक्षा चांगला पर्याय सापडला", + "answerFour": "वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही", + "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" + }, + "questionTwo": { + "question": "भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?", + "answerOne": "खूप शक्यता आहे", + "answerTwo": "काहीशी शक्यता आहे", + "answerThree": "निश्चित नाही", + "answerFour": "अल्प शक्यता", + "answerFive": "एकदम कमी शक्यता" + }, + "questionThree": { + "question": "तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?", + "answerOne": "अनेक वापरकर्त्यांशी सहकार्य", + "answerTwo": "लांब कालावधीची आवृत्ती इतिहास", + "answerThree": "अमर्यादित AI प्रतिसाद", + "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" + }, + "questionFour": { + "question": "@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?", + "answerOne": "खूप छान", + "answerTwo": "चांगला", + "answerThree": "सरासरी", + "answerFour": "सरासरीपेक्षा कमी", + "answerFive": "असंतोषजनक" + } +}, + "common": { + "uploadingFile": "फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका", + "uploadNotionSuccess": "तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल", + "reset": "रीसेट करा" +}, + "menu": { + "appearance": "दृश्यरूप", + "language": "भाषा", + "user": "वापरकर्ता", + "files": "फाईल्स", + "notifications": "सूचना", + "open": "सेटिंग्ज उघडा", + "logout": "लॉगआउट", + "logoutPrompt": "तुम्हाला नक्की लॉगआउट करायचे आहे का?", + "selfEncryptionLogoutPrompt": "तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे", + "syncSetting": "सिंक्रोनायझेशन सेटिंग", + "cloudSettings": "क्लाऊड सेटिंग्ज", + "enableSync": "सिंक्रोनायझेशन सक्षम करा", + "enableSyncLog": "सिंक लॉगिंग सक्षम करा", + "enableSyncLogWarning": "सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा", + "enableEncrypt": "डेटा एन्क्रिप्ट करा", + "cloudURL": "बेस URL", + "webURL": "वेब URL", + "invalidCloudURLScheme": "अवैध स्कीम", + "cloudServerType": "क्लाऊड सर्व्हर", + "cloudServerTypeTip": "कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते", + "cloudLocal": "स्थानिक", + "cloudAppFlowy": "@:appName Cloud", + "cloudAppFlowySelfHost": "@:appName क्लाऊड सेल्फ-होस्टेड", + "appFlowyCloudUrlCanNotBeEmpty": "क्लाऊड URL रिकामा असू शकत नाही", + "clickToCopy": "क्लिपबोर्डवर कॉपी करा", + "selfHostStart": "जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा", + "selfHostContent": "दस्तऐवज", + "selfHostEnd": "तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी", + "pleaseInputValidURL": "कृपया वैध URL टाका", + "changeUrl": "सेल्फ-होस्टेड URL {} मध्ये बदला", + "cloudURLHint": "तुमच्या सर्व्हरचा बेस URL टाका", + "webURLHint": "तुमच्या वेब सर्व्हरचा बेस URL टाका", + "cloudWSURL": "वेबसॉकेट URL", + "cloudWSURLHint": "तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका", + "restartApp": "अ‍ॅप रीस्टार्ट करा", + "restartAppTip": "बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.", + "changeServerTip": "सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे", + "enableEncryptPrompt": "तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा", + "inputEncryptPrompt": "कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:", + "clickToCopySecret": "गुप्तकी कॉपी करण्यासाठी क्लिक करा", + "configServerSetting": "तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा", + "configServerGuide": "`Quick Start` निवडल्यानंतर, `Settings` → \"Cloud Settings\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.", + "inputTextFieldHint": "तुमची गुप्तकी", + "historicalUserList": "वापरकर्ता लॉगिन इतिहास", + "historicalUserListTooltip": "ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात", + "openHistoricalUser": "अनामिक खाते उघडण्यासाठी क्लिक करा", + "customPathPrompt": "@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.", + "importAppFlowyData": "बाह्य @:appName फोल्डरमधून डेटा आयात करा", + "importingAppFlowyDataTip": "डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका", + "importAppFlowyDataDescription": "बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा", + "importSuccess": "@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला", + "importFailed": "@:appName डेटा फोल्डर आयात करण्यात अयशस्वी", + "importGuide": "अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा" +}, + "notifications": { + "enableNotifications": { + "label": "सूचना सक्षम करा", + "hint": "स्थानिक सूचना दिसू नयेत यासाठी बंद करा." + }, + "showNotificationsIcon": { + "label": "सूचना चिन्ह दाखवा", + "hint": "साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा." + }, + "archiveNotifications": { + "allSuccess": "सर्व सूचना यशस्वीरित्या संग्रहित केल्या", + "success": "सूचना यशस्वीरित्या संग्रहित केली" + }, + "markAsReadNotifications": { + "allSuccess": "सर्व वाचलेल्या म्हणून चिन्हांकित केल्या", + "success": "वाचलेले म्हणून चिन्हांकित केले" + }, + "action": { + "markAsRead": "वाचलेले म्हणून चिन्हांकित करा", + "multipleChoice": "अधिक निवडा", + "archive": "संग्रहित करा" + }, + "settings": { + "settings": "सेटिंग्ज", + "markAllAsRead": "सर्व वाचलेले म्हणून चिन्हांकित करा", + "archiveAll": "सर्व संग्रहित करा" + }, + "emptyInbox": { + "title": "इनबॉक्स झिरो!", + "description": "इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा." + }, + "emptyUnread": { + "title": "कोणतीही न वाचलेली सूचना नाही", + "description": "तुम्ही सर्व वाचले आहे!" + }, + "emptyArchived": { + "title": "कोणतीही संग्रहित सूचना नाही", + "description": "संग्रहित सूचना इथे दिसतील." + }, + "tabs": { + "inbox": "इनबॉक्स", + "unread": "न वाचलेले", + "archived": "संग्रहित" + }, + "refreshSuccess": "सूचना यशस्वीरित्या रीफ्रेश केल्या", + "titles": { + "notifications": "सूचना", + "reminder": "रिमाइंडर" + } +}, + "appearance": { + "resetSetting": "रीसेट", + "fontFamily": { + "label": "फॉन्ट फॅमिली", + "search": "शोध", + "defaultFont": "सिस्टम" + }, + "themeMode": { + "label": "थीम मोड", + "light": "लाइट मोड", + "dark": "डार्क मोड", + "system": "सिस्टमशी जुळवा" + }, + "fontScaleFactor": "फॉन्ट स्केल घटक", + "displaySize": "डिस्प्ले आकार", + "documentSettings": { + "cursorColor": "डॉक्युमेंट कर्सरचा रंग", + "selectionColor": "डॉक्युमेंट निवडीचा रंग", + "width": "डॉक्युमेंटची रुंदी", + "changeWidth": "बदला", + "pickColor": "रंग निवडा", + "colorShade": "रंगाची छटा", + "opacity": "अपारदर्शकता", + "hexEmptyError": "Hex रंग रिकामा असू शकत नाही", + "hexLengthError": "Hex व्हॅल्यू 6 अंकांची असावी", + "hexInvalidError": "अवैध Hex व्हॅल्यू", + "opacityEmptyError": "अपारदर्शकता रिकामी असू शकत नाही", + "opacityRangeError": "अपारदर्शकता 1 ते 100 दरम्यान असावी", + "app": "अ‍ॅप", + "flowy": "Flowy", + "apply": "लागू करा" + }, + "layoutDirection": { + "label": "लेआउट दिशा", + "hint": "तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.", + "ltr": "LTR", + "rtl": "RTL" + }, + "textDirection": { + "label": "मूलभूत मजकूर दिशा", + "hint": "मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "स्वयं", + "fallback": "लेआउट दिशेशी जुळवा" + }, + "themeUpload": { + "button": "अपलोड", + "uploadTheme": "थीम अपलोड करा", + "description": "खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.", + "loading": "कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...", + "uploadSuccess": "तुमची थीम यशस्वीरित्या अपलोड झाली आहे", + "deletionFailure": "थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.", + "filePickerDialogTitle": ".flowy_plugin फाईल निवडा", + "urlUploadFailure": "URL उघडण्यात अयशस्वी: {}" + }, + "theme": "थीम", + "builtInsLabel": "अंतर्गत थीम्स", + "pluginsLabel": "प्लगइन्स", + "dateFormat": { + "label": "दिनांक फॉरमॅट", + "local": "स्थानिक", + "us": "US", + "iso": "ISO", + "friendly": "अनौपचारिक", + "dmy": "D/M/Y" + }, + "timeFormat": { + "label": "वेळ फॉरमॅट", + "twelveHour": "१२ तास", + "twentyFourHour": "२४ तास" + }, + "showNamingDialogWhenCreatingPage": "पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा", + "enableRTLToolbarItems": "RTL टूलबार आयटम्स सक्षम करा", + "members": { + "title": "सदस्य सेटिंग्ज", + "inviteMembers": "सदस्यांना आमंत्रण द्या", + "inviteHint": "ईमेलद्वारे आमंत्रण द्या", + "sendInvite": "आमंत्रण पाठवा", + "copyInviteLink": "आमंत्रण दुवा कॉपी करा", + "label": "सदस्य", + "user": "वापरकर्ता", + "role": "भूमिका", + "removeFromWorkspace": "वर्कस्पेसमधून काढा", + "removeFromWorkspaceSuccess": "वर्कस्पेसमधून यशस्वीरित्या काढले", + "removeFromWorkspaceFailed": "वर्कस्पेसमधून काढण्यात अयशस्वी", + "owner": "मालक", + "guest": "अतिथी", + "member": "सदस्य", + "memberHintText": "सदस्य पृष्ठे वाचू व संपादित करू शकतो", + "guestHintText": "अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.", + "emailInvalidError": "अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा", + "emailSent": "ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा", + "members": "सदस्य", + "membersCount": { + "zero": "{} सदस्य", + "one": "{} सदस्य", + "other": "{} सदस्य" + }, + "inviteFailedDialogTitle": "आमंत्रण पाठवण्यात अयशस्वी", + "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.", + "inviteFailedMemberLimitMobile": "तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.", + "memberLimitExceeded": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया ", + "memberLimitExceededUpgrade": "अपग्रेड करा", + "memberLimitExceededPro": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा", + "memberLimitExceededProContact": "support@appflowy.io", + "failedToAddMember": "सदस्य जोडण्यात अयशस्वी", + "addMemberSuccess": "सदस्य यशस्वीरित्या जोडला गेला", + "removeMember": "सदस्य काढा", + "areYouSureToRemoveMember": "तुम्हाला हा सदस्य काढायचा आहे का?", + "inviteMemberSuccess": "आमंत्रण यशस्वीरित्या पाठवले गेले", + "failedToInviteMember": "सदस्य आमंत्रित करण्यात अयशस्वी", + "workspaceMembersError": "अरे! काहीतरी चूक झाली आहे", + "workspaceMembersErrorDescription": "आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा" + } +}, + "files": { + "copy": "कॉपी करा", + "defaultLocation": "फाईल्स आणि डेटाचा संचय स्थान", + "exportData": "तुमचा डेटा निर्यात करा", + "doubleTapToCopy": "पथ कॉपी करण्यासाठी दोनदा टॅप करा", + "restoreLocation": "@:appName चे मूळ स्थान पुनर्संचयित करा", + "customizeLocation": "इतर फोल्डर उघडा", + "restartApp": "बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.", + "exportDatabase": "डेटाबेस निर्यात करा", + "selectFiles": "निर्यात करण्यासाठी फाईल्स निवडा", + "selectAll": "सर्व निवडा", + "deselectAll": "सर्व निवड रद्द करा", + "createNewFolder": "नवीन फोल्डर तयार करा", + "createNewFolderDesc": "तुमचा डेटा कुठे साठवायचा हे सांगा", + "defineWhereYourDataIsStored": "तुमचा डेटा कुठे साठवला जातो हे ठरवा", + "open": "उघडा", + "openFolder": "आधीक फोल्डर उघडा", + "openFolderDesc": "तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा", + "folderHintText": "फोल्डरचे नाव", + "location": "नवीन फोल्डर तयार करत आहे", + "locationDesc": "तुमच्या @:appName डेटासाठी नाव निवडा", + "browser": "ब्राउझ करा", + "create": "तयार करा", + "set": "सेट करा", + "folderPath": "फोल्डर साठवण्याचा मार्ग", + "locationCannotBeEmpty": "मार्ग रिकामा असू शकत नाही", + "pathCopiedSnackbar": "फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!", + "changeLocationTooltips": "डेटा डिरेक्टरी बदला", + "change": "बदला", + "openLocationTooltips": "इतर डेटा डिरेक्टरी उघडा", + "openCurrentDataFolder": "सध्याची डेटा डिरेक्टरी उघडा", + "recoverLocationTooltips": "@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा", + "exportFileSuccess": "फाईल यशस्वीरित्या निर्यात झाली!", + "exportFileFail": "फाईल निर्यात करण्यात अयशस्वी!", + "export": "निर्यात करा", + "clearCache": "कॅशे साफ करा", + "clearCacheDesc": "प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.", + "areYouSureToClearCache": "तुम्हाला नक्की कॅशे साफ करायची आहे का?", + "clearCacheSuccess": "कॅशे यशस्वीरित्या साफ झाली!" +}, + "user": { + "name": "नाव", + "email": "ईमेल", + "tooltipSelectIcon": "चिन्ह निवडा", + "selectAnIcon": "चिन्ह निवडा", + "pleaseInputYourOpenAIKey": "कृपया तुमची AI की टाका", + "clickToLogout": "सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा" +}, + "mobile": { + "personalInfo": "वैयक्तिक माहिती", + "username": "वापरकर्तानाव", + "usernameEmptyError": "वापरकर्तानाव रिकामे असू शकत नाही", + "about": "विषयी", + "pushNotifications": "पुश सूचना", + "support": "सपोर्ट", + "joinDiscord": "Discord मध्ये सहभागी व्हा", + "privacyPolicy": "गोपनीयता धोरण", + "userAgreement": "वापरकर्ता करार", + "termsAndConditions": "अटी व शर्ती", + "userprofileError": "वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी", + "userprofileErrorDescription": "कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.", + "selectLayout": "लेआउट निवडा", + "selectStartingDay": "सप्ताहाचा प्रारंभ दिवस निवडा", + "version": "आवृत्ती" +}, + "grid": { + "deleteView": "तुम्हाला हे दृश्य हटवायचे आहे का?", + "createView": "नवीन", + "title": { + "placeholder": "नाव नाही" + }, + "settings": { + "filter": "फिल्टर", + "sort": "क्रमवारी", + "sortBy": "यावरून क्रमवारी लावा", + "properties": "गुणधर्म", + "reorderPropertiesTooltip": "गुणधर्मांचे स्थान बदला", + "group": "समूह", + "addFilter": "फिल्टर जोडा", + "deleteFilter": "फिल्टर हटवा", + "filterBy": "यावरून फिल्टर करा", + "typeAValue": "मूल्य लिहा...", + "layout": "लेआउट", + "compactMode": "कॉम्पॅक्ट मोड", + "databaseLayout": "लेआउट", + "viewList": { + "zero": "० दृश्ये", + "one": "{count} दृश्य", + "other": "{count} दृश्ये" + }, + "editView": "दृश्य संपादित करा", + "boardSettings": "बोर्ड सेटिंग", + "calendarSettings": "कॅलेंडर सेटिंग", + "createView": "नवीन दृश्य", + "duplicateView": "दृश्याची प्रत बनवा", + "deleteView": "दृश्य हटवा", + "numberOfVisibleFields": "{} दर्शविले" + }, + "filter": { + "empty": "कोणतेही सक्रिय फिल्टर नाहीत", + "addFilter": "फिल्टर जोडा", + "cannotFindCreatableField": "फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही", + "conditon": "अट", + "where": "जिथे" + }, + "textFilter": { + "contains": "अंतर्भूत आहे", + "doesNotContain": "अंतर्भूत नाही", + "endsWith": "याने समाप्त होते", + "startWith": "याने सुरू होते", + "is": "आहे", + "isNot": "नाही", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही", + "choicechipPrefix": { + "isNot": "नाही", + "startWith": "याने सुरू होते", + "endWith": "याने समाप्त होते", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" + } + }, + "checkboxFilter": { + "isChecked": "निवडलेले आहे", + "isUnchecked": "निवडलेले नाही", + "choicechipPrefix": { + "is": "आहे" + } + }, + "checklistFilter": { + "isComplete": "पूर्ण झाले आहे", + "isIncomplted": "अपूर्ण आहे" + }, + "selectOptionFilter": { + "is": "आहे", + "isNot": "नाही", + "contains": "अंतर्भूत आहे", + "doesNotContain": "अंतर्भूत नाही", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" +}, +"dateFilter": { + "is": "या दिवशी आहे", + "before": "पूर्वी आहे", + "after": "नंतर आहे", + "onOrBefore": "या दिवशी किंवा त्याआधी आहे", + "onOrAfter": "या दिवशी किंवा त्यानंतर आहे", + "between": "दरम्यान आहे", + "empty": "रिकामे आहे", + "notEmpty": "रिकामे नाही", + "startDate": "सुरुवातीची तारीख", + "endDate": "शेवटची तारीख", + "choicechipPrefix": { + "before": "पूर्वी", + "after": "नंतर", + "between": "दरम्यान", + "onOrBefore": "या दिवशी किंवा त्याआधी", + "onOrAfter": "या दिवशी किंवा त्यानंतर", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" + } +}, +"numberFilter": { + "equal": "बरोबर आहे", + "notEqual": "बरोबर नाही", + "lessThan": "पेक्षा कमी आहे", + "greaterThan": "पेक्षा जास्त आहे", + "lessThanOrEqualTo": "किंवा कमी आहे", + "greaterThanOrEqualTo": "किंवा जास्त आहे", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" +}, +"field": { + "label": "गुणधर्म", + "hide": "गुणधर्म लपवा", + "show": "गुणधर्म दर्शवा", + "insertLeft": "डावीकडे जोडा", + "insertRight": "उजवीकडे जोडा", + "duplicate": "प्रत बनवा", + "delete": "हटवा", + "wrapCellContent": "पाठ लपेटा", + "clear": "सेल्स रिकामे करा", + "switchPrimaryFieldTooltip": "प्राथमिक फील्डचा प्रकार बदलू शकत नाही", + "textFieldName": "मजकूर", + "checkboxFieldName": "चेकबॉक्स", + "dateFieldName": "तारीख", + "updatedAtFieldName": "शेवटचे अपडेट", + "createdAtFieldName": "तयार झाले", + "numberFieldName": "संख्या", + "singleSelectFieldName": "सिंगल सिलेक्ट", + "multiSelectFieldName": "मल्टीसिलेक्ट", + "urlFieldName": "URL", + "checklistFieldName": "चेकलिस्ट", + "relationFieldName": "संबंध", + "summaryFieldName": "AI सारांश", + "timeFieldName": "वेळ", + "mediaFieldName": "फाईल्स आणि मीडिया", + "translateFieldName": "AI भाषांतर", + "translateTo": "मध्ये भाषांतर करा", + "numberFormat": "संख्या स्वरूप", + "dateFormat": "तारीख स्वरूप", + "includeTime": "वेळ जोडा", + "isRange": "शेवटची तारीख", + "dateFormatFriendly": "महिना दिवस, वर्ष", + "dateFormatISO": "वर्ष-महिना-दिनांक", + "dateFormatLocal": "महिना/दिवस/वर्ष", + "dateFormatUS": "वर्ष/महिना/दिवस", + "dateFormatDayMonthYear": "दिवस/महिना/वर्ष", + "timeFormat": "वेळ स्वरूप", + "invalidTimeFormat": "अवैध स्वरूप", + "timeFormatTwelveHour": "१२ तास", + "timeFormatTwentyFourHour": "२४ तास", + "clearDate": "तारीख हटवा", + "dateTime": "तारीख व वेळ", + "startDateTime": "सुरुवातीची तारीख व वेळ", + "endDateTime": "शेवटची तारीख व वेळ", + "failedToLoadDate": "तारीख मूल्य लोड करण्यात अयशस्वी", + "selectTime": "वेळ निवडा", + "selectDate": "तारीख निवडा", + "visibility": "दृश्यता", + "propertyType": "गुणधर्माचा प्रकार", + "addSelectOption": "पर्याय जोडा", + "typeANewOption": "नवीन पर्याय लिहा", + "optionTitle": "पर्याय", + "addOption": "पर्याय जोडा", + "editProperty": "गुणधर्म संपादित करा", + "newProperty": "नवीन गुणधर्म", + "openRowDocument": "पृष्ठ म्हणून उघडा", + "deleteFieldPromptMessage": "तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल", + "clearFieldPromptMessage": "तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील", + "newColumn": "नवीन कॉलम", + "format": "स्वरूप", + "reminderOnDateTooltip": "या सेलमध्ये अनुस्मारक शेड्यूल केले आहे", + "optionAlreadyExist": "पर्याय आधीच अस्तित्वात आहे" +}, + "rowPage": { + "newField": "नवीन फील्ड जोडा", + "fieldDragElementTooltip": "मेनू उघडण्यासाठी क्लिक करा", + "showHiddenFields": { + "one": "{count} लपलेले फील्ड दाखवा", + "many": "{count} लपलेली फील्ड दाखवा", + "other": "{count} लपलेली फील्ड दाखवा" + }, + "hideHiddenFields": { + "one": "{count} लपलेले फील्ड लपवा", + "many": "{count} लपलेली फील्ड लपवा", + "other": "{count} लपलेली फील्ड लपवा" + }, + "openAsFullPage": "पूर्ण पृष्ठ म्हणून उघडा", + "moreRowActions": "अधिक पंक्ती क्रिया" +}, +"sort": { + "ascending": "चढत्या क्रमाने", + "descending": "उतरत्या क्रमाने", + "by": "द्वारे", + "empty": "सक्रिय सॉर्ट्स नाहीत", + "cannotFindCreatableField": "सॉर्टसाठी योग्य फील्ड सापडले नाही", + "deleteAllSorts": "सर्व सॉर्ट्स हटवा", + "addSort": "सॉर्ट जोडा", + "sortsActive": "सॉर्टिंग करत असताना {intention} करू शकत नाही", + "removeSorting": "या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?", + "fieldInUse": "या फील्डवर आधीच सॉर्टिंग चालू आहे" +}, +"row": { + "label": "पंक्ती", + "duplicate": "प्रत बनवा", + "delete": "हटवा", + "titlePlaceholder": "शीर्षक नाही", + "textPlaceholder": "रिक्त", + "copyProperty": "गुणधर्म क्लिपबोर्डवर कॉपी केला", + "count": "संख्या", + "newRow": "नवीन पंक्ती", + "loadMore": "अधिक लोड करा", + "action": "क्रिया", + "add": "खाली जोडा वर क्लिक करा", + "drag": "हलवण्यासाठी ड्रॅग करा", + "deleteRowPrompt": "तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "deleteCardPrompt": "तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "dragAndClick": "हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा", + "insertRecordAbove": "वर रेकॉर्ड जोडा", + "insertRecordBelow": "खाली रेकॉर्ड जोडा", + "noContent": "माहिती नाही", + "reorderRowDescription": "पंक्तीचे पुन्हा क्रमांकन", + "createRowAboveDescription": "वर पंक्ती तयार करा", + "createRowBelowDescription": "खाली पंक्ती जोडा" +}, +"selectOption": { + "create": "तयार करा", + "purpleColor": "जांभळा", + "pinkColor": "गुलाबी", + "lightPinkColor": "फिकट गुलाबी", + "orangeColor": "नारंगी", + "yellowColor": "पिवळा", + "limeColor": "लिंबू", + "greenColor": "हिरवा", + "aquaColor": "आक्वा", + "blueColor": "निळा", + "deleteTag": "टॅग हटवा", + "colorPanelTitle": "रंग", + "panelTitle": "पर्याय निवडा किंवा नवीन तयार करा", + "searchOption": "पर्याय शोधा", + "searchOrCreateOption": "पर्याय शोधा किंवा तयार करा", + "createNew": "नवीन तयार करा", + "orSelectOne": "किंवा पर्याय निवडा", + "typeANewOption": "नवीन पर्याय टाइप करा", + "tagName": "टॅग नाव" +}, +"checklist": { + "taskHint": "कार्याचे वर्णन", + "addNew": "नवीन कार्य जोडा", + "submitNewTask": "तयार करा", + "hideComplete": "पूर्ण कार्ये लपवा", + "showComplete": "सर्व कार्ये दाखवा" +}, +"url": { + "launch": "बाह्य ब्राउझरमध्ये लिंक उघडा", + "copy": "लिंक क्लिपबोर्डवर कॉपी करा", + "textFieldHint": "URL टाका", + "copiedNotification": "क्लिपबोर्डवर कॉपी केले!" +}, +"relation": { + "relatedDatabasePlaceLabel": "संबंधित डेटाबेस", + "relatedDatabasePlaceholder": "काही नाही", + "inRelatedDatabase": "या मध्ये", + "rowSearchTextFieldPlaceholder": "शोध", + "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:", + "emptySearchResult": "कोणतीही नोंद सापडली नाही", + "linkedRowListLabel": "{count} लिंक केलेल्या पंक्ती", + "unlinkedRowListLabel": "आणखी एक पंक्ती लिंक करा" +}, +"menuName": "ग्रिड", +"referencedGridPrefix": "दृश्य", +"calculate": "गणना करा", +"calculationTypeLabel": { + "none": "काही नाही", + "average": "सरासरी", + "max": "कमाल", + "median": "मध्यम", + "min": "किमान", + "sum": "बेरीज", + "count": "मोजणी", + "countEmpty": "रिकाम्यांची मोजणी", + "countEmptyShort": "रिक्त", + "countNonEmpty": "रिक्त नसलेल्यांची मोजणी", + "countNonEmptyShort": "भरलेले" +}, +"media": { + "rename": "पुन्हा नाव द्या", + "download": "डाउनलोड करा", + "expand": "मोठे करा", + "delete": "हटवा", + "moreFilesHint": "+{}", + "addFileOrImage": "फाईल किंवा लिंक जोडा", + "attachmentsHint": "{}", + "addFileMobile": "फाईल जोडा", + "extraCount": "+{}", + "deleteFileDescription": "तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "showFileNames": "फाईलचे नाव दाखवा", + "downloadSuccess": "फाईल डाउनलोड झाली", + "downloadFailedToken": "फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध", + "setAsCover": "कव्हर म्हणून सेट करा", + "openInBrowser": "ब्राउझरमध्ये उघडा", + "embedLink": "फाईल लिंक एम्बेड करा" + } +}, + "document": { + "menuName": "दस्तऐवज", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "creating": "तयार करत आहे...", + "slashMenu": { + "board": { + "selectABoardToLinkTo": "लिंक करण्यासाठी बोर्ड निवडा", + "createANewBoard": "नवीन बोर्ड तयार करा" + }, + "grid": { + "selectAGridToLinkTo": "लिंक करण्यासाठी ग्रिड निवडा", + "createANewGrid": "नवीन ग्रिड तयार करा" + }, + "calendar": { + "selectACalendarToLinkTo": "लिंक करण्यासाठी दिनदर्शिका निवडा", + "createANewCalendar": "नवीन दिनदर्शिका तयार करा" + }, + "document": { + "selectADocumentToLinkTo": "लिंक करण्यासाठी दस्तऐवज निवडा" + }, + "name": { + "textStyle": "मजकुराची शैली", + "list": "यादी", + "toggle": "टॉगल", + "fileAndMedia": "फाईल व मीडिया", + "simpleTable": "सोपे टेबल", + "visuals": "दृश्य घटक", + "document": "दस्तऐवज", + "advanced": "प्रगत", + "text": "मजकूर", + "heading1": "शीर्षक 1", + "heading2": "शीर्षक 2", + "heading3": "शीर्षक 3", + "image": "प्रतिमा", + "bulletedList": "बुलेट यादी", + "numberedList": "क्रमांकित यादी", + "todoList": "करण्याची यादी", + "doc": "दस्तऐवज", + "linkedDoc": "पृष्ठाशी लिंक करा", + "grid": "ग्रिड", + "linkedGrid": "लिंक केलेला ग्रिड", + "kanban": "कानबन", + "linkedKanban": "लिंक केलेला कानबन", + "calendar": "दिनदर्शिका", + "linkedCalendar": "लिंक केलेली दिनदर्शिका", + "quote": "उद्धरण", + "divider": "विभाजक", + "table": "टेबल", + "callout": "महत्त्वाचा मजकूर", + "outline": "रूपरेषा", + "mathEquation": "गणिती समीकरण", + "code": "कोड", + "toggleList": "टॉगल यादी", + "toggleHeading1": "टॉगल शीर्षक 1", + "toggleHeading2": "टॉगल शीर्षक 2", + "toggleHeading3": "टॉगल शीर्षक 3", + "emoji": "इमोजी", + "aiWriter": "AI ला काहीही विचारा", + "dateOrReminder": "दिनांक किंवा स्मरणपत्र", + "photoGallery": "फोटो गॅलरी", + "file": "फाईल", + "twoColumns": "२ स्तंभ", + "threeColumns": "३ स्तंभ", + "fourColumns": "४ स्तंभ" + }, + "subPage": { + "name": "दस्तऐवज", + "keyword1": "उपपृष्ठ", + "keyword2": "पृष्ठ", + "keyword3": "चाइल्ड पृष्ठ", + "keyword4": "पृष्ठ जोडा", + "keyword5": "एम्बेड पृष्ठ", + "keyword6": "नवीन पृष्ठ", + "keyword7": "पृष्ठ तयार करा", + "keyword8": "दस्तऐवज" + } + }, + "selectionMenu": { + "outline": "रूपरेषा", + "codeBlock": "कोड ब्लॉक" + }, + "plugins": { + "referencedBoard": "संदर्भित बोर्ड", + "referencedGrid": "संदर्भित ग्रिड", + "referencedCalendar": "संदर्भित दिनदर्शिका", + "referencedDocument": "संदर्भित दस्तऐवज", + "aiWriter": { + "userQuestion": "AI ला काहीही विचारा", + "continueWriting": "लेखन सुरू ठेवा", + "fixSpelling": "स्पेलिंग व व्याकरण सुधारणा", + "improveWriting": "लेखन सुधारित करा", + "summarize": "सारांश द्या", + "explain": "स्पष्टीकरण द्या", + "makeShorter": "लहान करा", + "makeLonger": "मोठे करा" + }, + "autoGeneratorMenuItemName": "AI लेखक", +"autoGeneratorTitleName": "AI: काहीही लिहिण्यासाठी AI ला विचारा...", +"autoGeneratorLearnMore": "अधिक जाणून घ्या", +"autoGeneratorGenerate": "उत्पन्न करा", +"autoGeneratorHintText": "AI ला विचारा...", +"autoGeneratorCantGetOpenAIKey": "AI की मिळवू शकलो नाही", +"autoGeneratorRewrite": "पुन्हा लिहा", +"smartEdit": "AI ला विचारा", +"aI": "AI", +"smartEditFixSpelling": "स्पेलिंग आणि व्याकरण सुधारा", +"warning": "⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.", +"smartEditSummarize": "सारांश द्या", +"smartEditImproveWriting": "लेखन सुधारित करा", +"smartEditMakeLonger": "लांब करा", +"smartEditCouldNotFetchResult": "AI कडून उत्तर मिळवता आले नाही", +"smartEditCouldNotFetchKey": "AI की मिळवता आली नाही", +"smartEditDisabled": "सेटिंग्जमध्ये AI कनेक्ट करा", +"appflowyAIEditDisabled": "AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा", +"discardResponse": "AI उत्तर फेकून द्यायचं आहे का?", +"createInlineMathEquation": "समीकरण तयार करा", +"fonts": "फॉन्ट्स", +"insertDate": "तारीख जोडा", +"emoji": "इमोजी", +"toggleList": "टॉगल यादी", +"emptyToggleHeading": "रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.", +"emptyToggleList": "रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.", +"emptyToggleHeadingWeb": "रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा", +"quoteList": "उद्धरण यादी", +"numberedList": "क्रमांकित यादी", +"bulletedList": "बुलेट यादी", +"todoList": "करण्याची यादी", +"callout": "ठळक मजकूर", +"simpleTable": { + "moreActions": { + "color": "रंग", + "align": "पंक्तिबद्ध करा", + "delete": "हटा", + "duplicate": "डुप्लिकेट करा", + "insertLeft": "डावीकडे घाला", + "insertRight": "उजवीकडे घाला", + "insertAbove": "वर घाला", + "insertBelow": "खाली घाला", + "headerColumn": "हेडर स्तंभ", + "headerRow": "हेडर ओळ", + "clearContents": "सामग्री साफ करा", + "setToPageWidth": "पृष्ठाच्या रुंदीप्रमाणे सेट करा", + "distributeColumnsWidth": "स्तंभ समान करा", + "duplicateRow": "ओळ डुप्लिकेट करा", + "duplicateColumn": "स्तंभ डुप्लिकेट करा", + "textColor": "मजकूराचा रंग", + "cellBackgroundColor": "सेलचा पार्श्वभूमी रंग", + "duplicateTable": "टेबल डुप्लिकेट करा" + }, + "clickToAddNewRow": "नवीन ओळ जोडण्यासाठी क्लिक करा", + "clickToAddNewColumn": "नवीन स्तंभ जोडण्यासाठी क्लिक करा", + "clickToAddNewRowAndColumn": "नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा", + "headerName": { + "table": "टेबल", + "alignText": "मजकूर पंक्तिबद्ध करा" + } +}, +"cover": { + "changeCover": "कव्हर बदला", + "colors": "रंग", + "images": "प्रतिमा", + "clearAll": "सर्व साफ करा", + "abstract": "ऍबस्ट्रॅक्ट", + "addCover": "कव्हर जोडा", + "addLocalImage": "स्थानिक प्रतिमा जोडा", + "invalidImageUrl": "अवैध प्रतिमा URL", + "failedToAddImageToGallery": "प्रतिमा गॅलरीत जोडता आली नाही", + "enterImageUrl": "प्रतिमा URL लिहा", + "add": "जोडा", + "back": "मागे", + "saveToGallery": "गॅलरीत जतन करा", + "removeIcon": "आयकॉन काढा", + "removeCover": "कव्हर काढा", + "pasteImageUrl": "प्रतिमा URL पेस्ट करा", + "or": "किंवा", + "pickFromFiles": "फाईल्समधून निवडा", + "couldNotFetchImage": "प्रतिमा मिळवता आली नाही", + "imageSavingFailed": "प्रतिमा जतन करणे अयशस्वी", + "addIcon": "आयकॉन जोडा", + "changeIcon": "आयकॉन बदला", + "coverRemoveAlert": "हे हटवल्यानंतर कव्हरमधून काढले जाईल.", + "alertDialogConfirmation": "तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?" +}, +"mathEquation": { + "name": "गणिती समीकरण", + "addMathEquation": "TeX समीकरण जोडा", + "editMathEquation": "गणिती समीकरण संपादित करा" +}, +"optionAction": { + "click": "क्लिक", + "toOpenMenu": "मेनू उघडण्यासाठी", + "drag": "ओढा", + "toMove": "हलवण्यासाठी", + "delete": "हटा", + "duplicate": "डुप्लिकेट करा", + "turnInto": "मध्ये बदला", + "moveUp": "वर हलवा", + "moveDown": "खाली हलवा", + "color": "रंग", + "align": "पंक्तिबद्ध करा", + "left": "डावीकडे", + "center": "मध्यभागी", + "right": "उजवीकडे", + "defaultColor": "डिफॉल्ट", + "depth": "खोली", + "copyLinkToBlock": "ब्लॉकसाठी लिंक कॉपी करा" +}, + "image": { + "addAnImage": "प्रतिमा जोडा", + "copiedToPasteBoard": "प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "addAnImageDesktop": "प्रतिमा जोडा", + "addAnImageMobile": "एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा", + "dropImageToInsert": "प्रतिमा ड्रॉप करून जोडा", + "imageUploadFailed": "प्रतिमा अपलोड करण्यात अयशस्वी", + "imageDownloadFailed": "प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "imageDownloadFailedToken": "युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "errorCode": "त्रुटी कोड" +}, +"photoGallery": { + "name": "फोटो गॅलरी", + "imageKeyword": "प्रतिमा", + "imageGalleryKeyword": "प्रतिमा गॅलरी", + "photoKeyword": "फोटो", + "photoBrowserKeyword": "फोटो ब्राउझर", + "galleryKeyword": "गॅलरी", + "addImageTooltip": "प्रतिमा जोडा", + "changeLayoutTooltip": "लेआउट बदला", + "browserLayout": "ब्राउझर", + "gridLayout": "ग्रिड", + "deleteBlockTooltip": "संपूर्ण गॅलरी हटवा" +}, +"math": { + "copiedToPasteBoard": "समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे" +}, +"urlPreview": { + "copiedToPasteBoard": "लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "convertToLink": "एंबेड लिंकमध्ये रूपांतर करा" +}, +"outline": { + "addHeadingToCreateOutline": "सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.", + "noMatchHeadings": "जुळणारी शीर्षके आढळली नाहीत." +}, +"table": { + "addAfter": "नंतर जोडा", + "addBefore": "आधी जोडा", + "delete": "हटा", + "clear": "सामग्री साफ करा", + "duplicate": "डुप्लिकेट करा", + "bgColor": "पार्श्वभूमीचा रंग" +}, +"contextMenu": { + "copy": "कॉपी करा", + "cut": "कापा", + "paste": "पेस्ट करा", + "pasteAsPlainText": "साध्या मजकूराच्या स्वरूपात पेस्ट करा" +}, +"action": "कृती", +"database": { + "selectDataSource": "डेटा स्रोत निवडा", + "noDataSource": "डेटा स्रोत नाही", + "selectADataSource": "डेटा स्रोत निवडा", + "toContinue": "पुढे जाण्यासाठी", + "newDatabase": "नवीन डेटाबेस", + "linkToDatabase": "डेटाबेसशी लिंक करा" +}, +"date": "तारीख", +"video": { + "label": "व्हिडिओ", + "emptyLabel": "व्हिडिओ जोडा", + "placeholder": "व्हिडिओ लिंक पेस्ट करा", + "copiedToPasteBoard": "व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "insertVideo": "व्हिडिओ जोडा", + "invalidVideoUrl": "ही URL सध्या समर्थित नाही.", + "invalidVideoUrlYouTube": "YouTube सध्या समर्थित नाही.", + "supportedFormats": "समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" +}, +"file": { + "name": "फाईल", + "uploadTab": "अपलोड", + "uploadMobile": "फाईल निवडा", + "uploadMobileGallery": "फोटो गॅलरीमधून", + "networkTab": "लिंक एम्बेड करा", + "placeholderText": "फाईल अपलोड किंवा एम्बेड करा", + "placeholderDragging": "फाईल ड्रॉप करून अपलोड करा", + "dropFileToUpload": "फाईल ड्रॉप करून अपलोड करा", + "fileUploadHint": "फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा ", + "fileUploadHintSuffix": "ब्राउझ करा", + "networkHint": "फाईल लिंक पेस्ट करा", + "networkUrlInvalid": "अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.", + "networkAction": "एम्बेड", + "fileTooBigError": "फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा", + "renameFile": { + "title": "फाईलचे नाव बदला", + "description": "या फाईलसाठी नवीन नाव लिहा", + "nameEmptyError": "फाईलचे नाव रिकामे असू शकत नाही." + }, + "uploadedAt": "{} रोजी अपलोड केले", + "linkedAt": "{} रोजी लिंक जोडली", + "failedToOpenMsg": "उघडण्यात अयशस्वी, फाईल सापडली नाही" +}, +"subPage": { + "handlingPasteHint": " - (पेस्ट प्रक्रिया करत आहे)", + "errors": { + "failedDeletePage": "पृष्ठ हटवण्यात अयशस्वी", + "failedCreatePage": "पृष्ठ तयार करण्यात अयशस्वी", + "failedMovePage": "हे पृष्ठ हलवण्यात अयशस्वी", + "failedDuplicatePage": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी", + "failedDuplicateFindView": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही" + } +}, + "cannotMoveToItsChildren": "मुलांमध्ये हलवू शकत नाही" +}, +"outlineBlock": { + "placeholder": "सामग्री सूची" +}, +"textBlock": { + "placeholder": "कमांडसाठी '/' टाइप करा" +}, +"title": { + "placeholder": "शीर्षक नाही" +}, +"imageBlock": { + "placeholder": "प्रतिमा जोडण्यासाठी क्लिक करा", + "upload": { + "label": "अपलोड", + "placeholder": "प्रतिमा अपलोड करण्यासाठी क्लिक करा" + }, + "url": { + "label": "प्रतिमेची URL", + "placeholder": "प्रतिमेची URL टाका" + }, + "ai": { + "label": "AI द्वारे प्रतिमा तयार करा", + "placeholder": "AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" + }, + "stability_ai": { + "label": "Stability AI द्वारे प्रतिमा तयार करा", + "placeholder": "Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" + }, + "support": "प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG", + "error": { + "invalidImage": "अवैध प्रतिमा", + "invalidImageSize": "प्रतिमेचा आकार 5MB पेक्षा कमी असावा", + "invalidImageFormat": "प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "अवैध प्रतिमेची URL", + "noImage": "अशी फाईल किंवा निर्देशिका नाही", + "multipleImagesFailed": "एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा" + }, + "embedLink": { + "label": "लिंक एम्बेड करा", + "placeholder": "प्रतिमेची लिंक पेस्ट करा किंवा टाका" + }, + "unsplash": { + "label": "Unsplash" + }, + "searchForAnImage": "प्रतिमा शोधा", + "pleaseInputYourOpenAIKey": "कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा", + "saveImageToGallery": "प्रतिमा जतन करा", + "failedToAddImageToGallery": "प्रतिमा जतन करण्यात अयशस्वी", + "successToAddImageToGallery": "प्रतिमा 'Photos' मध्ये जतन केली", + "unableToLoadImage": "प्रतिमा लोड करण्यात अयशस्वी", + "maximumImageSize": "कमाल प्रतिमा अपलोड आकार 10MB आहे", + "uploadImageErrorImageSizeTooBig": "प्रतिमेचा आकार 10MB पेक्षा कमी असावा", + "imageIsUploading": "प्रतिमा अपलोड होत आहे", + "openFullScreen": "पूर्ण स्क्रीनमध्ये उघडा", + "interactiveViewer": { + "toolbar": { + "previousImageTooltip": "मागील प्रतिमा", + "nextImageTooltip": "पुढील प्रतिमा", + "zoomOutTooltip": "लहान करा", + "zoomInTooltip": "मोठी करा", + "changeZoomLevelTooltip": "झूम पातळी बदला", + "openLocalImage": "प्रतिमा उघडा", + "downloadImage": "प्रतिमा डाउनलोड करा", + "closeViewer": "इंटरअॅक्टिव्ह व्ह्युअर बंद करा", + "scalePercentage": "{}%", + "deleteImageTooltip": "प्रतिमा हटवा" + } + } +}, + "codeBlock": { + "language": { + "label": "भाषा", + "placeholder": "भाषा निवडा", + "auto": "स्वयंचलित" + }, + "copyTooltip": "कॉपी करा", + "searchLanguageHint": "भाषा शोधा", + "codeCopiedSnackbar": "कोड क्लिपबोर्डवर कॉपी झाला!" +}, +"inlineLink": { + "placeholder": "लिंक पेस्ट करा किंवा टाका", + "openInNewTab": "नवीन टॅबमध्ये उघडा", + "copyLink": "लिंक कॉपी करा", + "removeLink": "लिंक काढा", + "url": { + "label": "लिंक URL", + "placeholder": "लिंक URL टाका" + }, + "title": { + "label": "लिंक शीर्षक", + "placeholder": "लिंक शीर्षक टाका" + } +}, +"mention": { + "placeholder": "कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...", + "page": { + "label": "पृष्ठाला लिंक करा", + "tooltip": "पृष्ठ उघडण्यासाठी क्लिक करा" + }, + "deleted": "हटवले गेले", + "deletedContent": "ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे", + "noAccess": "प्रवेश नाही", + "deletedPage": "हटवलेले पृष्ठ", + "trashHint": " - ट्रॅशमध्ये", + "morePages": "अजून पृष्ठे" +}, +"toolbar": { + "resetToDefaultFont": "डीफॉल्ट फॉन्टवर परत जा", + "textSize": "मजकूराचा आकार", + "textColor": "मजकूराचा रंग", + "h1": "मथळा 1", + "h2": "मथळा 2", + "h3": "मथळा 3", + "alignLeft": "डावीकडे संरेखित करा", + "alignRight": "उजवीकडे संरेखित करा", + "alignCenter": "मध्यभागी संरेखित करा", + "link": "लिंक", + "textAlign": "मजकूर संरेखन", + "moreOptions": "अधिक पर्याय", + "font": "फॉन्ट", + "inlineCode": "इनलाइन कोड", + "suggestions": "सूचना", + "turnInto": "मध्ये रूपांतरित करा", + "equation": "समीकरण", + "insert": "घाला", + "linkInputHint": "लिंक पेस्ट करा किंवा पृष्ठे शोधा", + "pageOrURL": "पृष्ठ किंवा URL", + "linkName": "लिंकचे नाव", + "linkNameHint": "लिंकचे नाव प्रविष्ट करा" +}, +"errorBlock": { + "theBlockIsNotSupported": "ब्लॉक सामग्री पार्स करण्यात अक्षम", + "clickToCopyTheBlockContent": "ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा", + "blockContentHasBeenCopied": "ब्लॉक सामग्री कॉपी केली आहे.", + "parseError": "{} ब्लॉक पार्स करताना त्रुटी आली.", + "copyBlockContent": "ब्लॉक सामग्री कॉपी करा" +}, +"mobilePageSelector": { + "title": "पृष्ठ निवडा", + "failedToLoad": "पृष्ठ यादी लोड करण्यात अयशस्वी", + "noPagesFound": "कोणतीही पृष्ठे सापडली नाहीत" +}, +"attachmentMenu": { + "choosePhoto": "फोटो निवडा", + "takePicture": "फोटो काढा", + "chooseFile": "फाईल निवडा" + } + }, + "board": { + "column": { + "label": "स्तंभ", + "createNewCard": "नवीन", + "renameGroupTooltip": "गटाचे नाव बदलण्यासाठी क्लिक करा", + "createNewColumn": "नवीन गट जोडा", + "addToColumnTopTooltip": "वर नवीन कार्ड जोडा", + "addToColumnBottomTooltip": "खाली नवीन कार्ड जोडा", + "renameColumn": "स्तंभाचे नाव बदला", + "hideColumn": "लपवा", + "newGroup": "नवीन गट", + "deleteColumn": "हटवा", + "deleteColumnConfirmation": "हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?" + }, + "hiddenGroupSection": { + "sectionTitle": "लपवलेले गट", + "collapseTooltip": "लपवलेले गट लपवा", + "expandTooltip": "लपवलेले गट पाहा" + }, + "cardDetail": "कार्ड तपशील", + "cardActions": "कार्ड क्रिया", + "cardDuplicated": "कार्डची प्रत तयार झाली", + "cardDeleted": "कार्ड हटवले गेले", + "showOnCard": "कार्ड तपशिलावर दाखवा", + "setting": "सेटिंग", + "propertyName": "गुणधर्माचे नाव", + "menuName": "बोर्ड", + "showUngrouped": "गटात नसलेली कार्ड्स दाखवा", + "ungroupedButtonText": "गट नसलेली", + "ungroupedButtonTooltip": "ज्या कार्ड्स कोणत्याही गटात नाहीत", + "ungroupedItemsTitle": "बोर्डमध्ये जोडण्यासाठी क्लिक करा", + "groupBy": "या आधारावर गट करा", + "groupCondition": "गट स्थिती", + "referencedBoardPrefix": "याचे दृश्य", + "notesTooltip": "नोट्स आहेत", + "mobile": { + "editURL": "URL संपादित करा", + "showGroup": "गट दाखवा", + "showGroupContent": "हा गट बोर्डवर दाखवायचा आहे का?", + "failedToLoad": "बोर्ड दृश्य लोड होण्यात अयशस्वी" + }, + "dateCondition": { + "weekOf": "{} - {} ची आठवडा", + "today": "आज", + "yesterday": "काल", + "tomorrow": "उद्या", + "lastSevenDays": "शेवटचे ७ दिवस", + "nextSevenDays": "पुढील ७ दिवस", + "lastThirtyDays": "शेवटचे ३० दिवस", + "nextThirtyDays": "पुढील ३० दिवस" + }, + "noGroup": "गटासाठी कोणताही गुणधर्म निवडलेला नाही", + "noGroupDesc": "बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे", + "media": { + "cardText": "{} {}", + "fallbackName": "फायली" + } +}, + "calendar": { + "menuName": "कॅलेंडर", + "defaultNewCalendarTitle": "नाव नाही", + "newEventButtonTooltip": "नवीन इव्हेंट जोडा", + "navigation": { + "today": "आज", + "jumpToday": "आजवर जा", + "previousMonth": "मागील महिना", + "nextMonth": "पुढील महिना", + "views": { + "day": "दिवस", + "week": "आठवडा", + "month": "महिना", + "year": "वर्ष" + } + }, + "mobileEventScreen": { + "emptyTitle": "सध्या कोणतेही इव्हेंट नाहीत", + "emptyBody": "या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा." + }, + "settings": { + "showWeekNumbers": "आठवड्याचे क्रमांक दाखवा", + "showWeekends": "सप्ताहांत दाखवा", + "firstDayOfWeek": "आठवड्याची सुरुवात", + "layoutDateField": "कॅलेंडर मांडणी दिनांकानुसार", + "changeLayoutDateField": "मांडणी फील्ड बदला", + "noDateTitle": "तारीख नाही", + "noDateHint": { + "zero": "नियोजित नसलेली इव्हेंट्स येथे दिसतील", + "one": "{count} नियोजित नसलेली इव्हेंट", + "other": "{count} नियोजित नसलेल्या इव्हेंट्स" + }, + "unscheduledEventsTitle": "नियोजित नसलेल्या इव्हेंट्स", + "clickToAdd": "कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा", + "name": "कॅलेंडर सेटिंग्ज", + "clickToOpen": "रेकॉर्ड उघडण्यासाठी क्लिक करा" + }, + "referencedCalendarPrefix": "याचे दृश्य", + "quickJumpYear": "या वर्षावर जा", + "duplicateEvent": "इव्हेंट डुप्लिकेट करा" +}, + "errorDialog": { + "title": "@:appName त्रुटी", + "howToFixFallback": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.", + "howToFixFallbackHint1": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया ", + "howToFixFallbackHint2": " पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.", + "github": "GitHub वर पहा" +}, +"search": { + "label": "शोध", + "sidebarSearchIcon": "पृष्ठ शोधा आणि पटकन जा", + "placeholder": { + "actions": "कृती शोधा..." + } +}, +"message": { + "copy": { + "success": "कॉपी झाले!", + "fail": "कॉपी करू शकत नाही" + } +}, +"unSupportBlock": "सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.", +"views": { + "deleteContentTitle": "तुम्हाला हे {pageType} हटवायचे आहे का?", + "deleteContentCaption": "हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता." +}, + "colors": { + "custom": "सानुकूल", + "default": "डीफॉल्ट", + "red": "लाल", + "orange": "संत्रा", + "yellow": "पिवळा", + "green": "हिरवा", + "blue": "निळा", + "purple": "जांभळा", + "pink": "गुलाबी", + "brown": "तपकिरी", + "gray": "करड्या रंगाचा" +}, + "emoji": { + "emojiTab": "इमोजी", + "search": "इमोजी शोधा", + "noRecent": "अलीकडील कोणतेही इमोजी नाहीत", + "noEmojiFound": "कोणतेही इमोजी सापडले नाहीत", + "filter": "फिल्टर", + "random": "योगायोगाने", + "selectSkinTone": "त्वचेचा टोन निवडा", + "remove": "इमोजी काढा", + "categories": { + "smileys": "स्मायली आणि भावना", + "people": "लोक", + "animals": "प्राणी आणि निसर्ग", + "food": "अन्न", + "activities": "क्रिया", + "places": "स्थळे", + "objects": "वस्तू", + "symbols": "चिन्हे", + "flags": "ध्वज", + "nature": "निसर्ग", + "frequentlyUsed": "नेहमी वापरलेले" + }, + "skinTone": { + "default": "डीफॉल्ट", + "light": "हलका", + "mediumLight": "मध्यम-हलका", + "medium": "मध्यम", + "mediumDark": "मध्यम-गडद", + "dark": "गडद" + }, + "openSourceIconsFrom": "खुल्या स्रोताचे आयकॉन्स" +}, + "inlineActions": { + "noResults": "निकाल नाही", + "recentPages": "अलीकडील पृष्ठे", + "pageReference": "पृष्ठ संदर्भ", + "docReference": "दस्तऐवज संदर्भ", + "boardReference": "बोर्ड संदर्भ", + "calReference": "कॅलेंडर संदर्भ", + "gridReference": "ग्रिड संदर्भ", + "date": "तारीख", + "reminder": { + "groupTitle": "स्मरणपत्र", + "shortKeyword": "remind" + }, + "createPage": "\"{}\" उप-पृष्ठ तयार करा" +}, + "datePicker": { + "dateTimeFormatTooltip": "सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला", + "dateFormat": "तारीख फॉरमॅट", + "includeTime": "वेळ समाविष्ट करा", + "isRange": "शेवटची तारीख", + "timeFormat": "वेळ फॉरमॅट", + "clearDate": "तारीख साफ करा", + "reminderLabel": "स्मरणपत्र", + "selectReminder": "स्मरणपत्र निवडा", + "reminderOptions": { + "none": "काहीही नाही", + "atTimeOfEvent": "इव्हेंटच्या वेळी", + "fiveMinsBefore": "५ मिनिटे आधी", + "tenMinsBefore": "१० मिनिटे आधी", + "fifteenMinsBefore": "१५ मिनिटे आधी", + "thirtyMinsBefore": "३० मिनिटे आधी", + "oneHourBefore": "१ तास आधी", + "twoHoursBefore": "२ तास आधी", + "onDayOfEvent": "इव्हेंटच्या दिवशी", + "oneDayBefore": "१ दिवस आधी", + "twoDaysBefore": "२ दिवस आधी", + "oneWeekBefore": "१ आठवडा आधी", + "custom": "सानुकूल" + } +}, + "relativeDates": { + "yesterday": "काल", + "today": "आज", + "tomorrow": "उद्या", + "oneWeek": "१ आठवडा" +}, + "notificationHub": { + "title": "सूचना", + "mobile": { + "title": "अपडेट्स" + }, + "emptyTitle": "सर्व पूर्ण झाले!", + "emptyBody": "कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.", + "tabs": { + "inbox": "इनबॉक्स", + "upcoming": "आगामी" + }, + "actions": { + "markAllRead": "सर्व वाचलेल्या म्हणून चिन्हित करा", + "showAll": "सर्व", + "showUnreads": "न वाचलेल्या" + }, + "filters": { + "ascending": "आरोही", + "descending": "अवरोही", + "groupByDate": "तारीखेनुसार गटबद्ध करा", + "showUnreadsOnly": "फक्त न वाचलेल्या दाखवा", + "resetToDefault": "डीफॉल्टवर रीसेट करा" + } +}, + "reminderNotification": { + "title": "स्मरणपत्र", + "message": "तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!", + "tooltipDelete": "हटवा", + "tooltipMarkRead": "वाचले म्हणून चिन्हित करा", + "tooltipMarkUnread": "न वाचले म्हणून चिन्हित करा" +}, + "findAndReplace": { + "find": "शोधा", + "previousMatch": "मागील जुळणारे", + "nextMatch": "पुढील जुळणारे", + "close": "बंद करा", + "replace": "बदला", + "replaceAll": "सर्व बदला", + "noResult": "कोणतेही निकाल नाहीत", + "caseSensitive": "केस सेंसिटिव्ह", + "searchMore": "अधिक निकालांसाठी शोधा" +}, + "error": { + "weAreSorry": "आम्ही क्षमस्व आहोत", + "loadingViewError": "हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.", + "syncError": "इतर डिव्हाइसमधून डेटा सिंक झाला नाही", + "syncErrorHint": "कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.", + "clickToCopy": "एरर कोड कॉपी करण्यासाठी क्लिक करा" +}, + "editor": { + "bold": "जाड", + "bulletedList": "बुलेट यादी", + "bulletedListShortForm": "बुलेट", + "checkbox": "चेकबॉक्स", + "embedCode": "कोड एम्बेड करा", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "हायलाइट", + "color": "रंग", + "image": "प्रतिमा", + "date": "तारीख", + "page": "पृष्ठ", + "italic": "तिरका", + "link": "लिंक", + "numberedList": "क्रमांकित यादी", + "numberedListShortForm": "क्रमांकित", + "toggleHeading1ShortForm": "Toggle H1", + "toggleHeading2ShortForm": "Toggle H2", + "toggleHeading3ShortForm": "Toggle H3", + "quote": "कोट", + "strikethrough": "ओढून टाका", + "text": "मजकूर", + "underline": "अधोरेखित", + "fontColorDefault": "डीफॉल्ट", + "fontColorGray": "धूसर", + "fontColorBrown": "तपकिरी", + "fontColorOrange": "केशरी", + "fontColorYellow": "पिवळा", + "fontColorGreen": "हिरवा", + "fontColorBlue": "निळा", + "fontColorPurple": "जांभळा", + "fontColorPink": "पिंग", + "fontColorRed": "लाल", + "backgroundColorDefault": "डीफॉल्ट पार्श्वभूमी", + "backgroundColorGray": "धूसर पार्श्वभूमी", + "backgroundColorBrown": "तपकिरी पार्श्वभूमी", + "backgroundColorOrange": "केशरी पार्श्वभूमी", + "backgroundColorYellow": "पिवळी पार्श्वभूमी", + "backgroundColorGreen": "हिरवी पार्श्वभूमी", + "backgroundColorBlue": "निळी पार्श्वभूमी", + "backgroundColorPurple": "जांभळी पार्श्वभूमी", + "backgroundColorPink": "पिंग पार्श्वभूमी", + "backgroundColorRed": "लाल पार्श्वभूमी", + "backgroundColorLime": "लिंबू पार्श्वभूमी", + "backgroundColorAqua": "पाण्याचा पार्श्वभूमी", + "done": "पूर्ण", + "cancel": "रद्द करा", + "tint1": "टिंट 1", + "tint2": "टिंट 2", + "tint3": "टिंट 3", + "tint4": "टिंट 4", + "tint5": "टिंट 5", + "tint6": "टिंट 6", + "tint7": "टिंट 7", + "tint8": "टिंट 8", + "tint9": "टिंट 9", + "lightLightTint1": "जांभळा", + "lightLightTint2": "पिंग", + "lightLightTint3": "फिकट पिंग", + "lightLightTint4": "केशरी", + "lightLightTint5": "पिवळा", + "lightLightTint6": "लिंबू", + "lightLightTint7": "हिरवा", + "lightLightTint8": "पाणी", + "lightLightTint9": "निळा", + "urlHint": "URL", + "mobileHeading1": "Heading 1", + "mobileHeading2": "Heading 2", + "mobileHeading3": "Heading 3", + "mobileHeading4": "Heading 4", + "mobileHeading5": "Heading 5", + "mobileHeading6": "Heading 6", + "textColor": "मजकूराचा रंग", + "backgroundColor": "पार्श्वभूमीचा रंग", + "addYourLink": "तुमची लिंक जोडा", + "openLink": "लिंक उघडा", + "copyLink": "लिंक कॉपी करा", + "removeLink": "लिंक काढा", + "editLink": "लिंक संपादित करा", + "linkText": "मजकूर", + "linkTextHint": "कृपया मजकूर प्रविष्ट करा", + "linkAddressHint": "कृपया URL प्रविष्ट करा", + "highlightColor": "हायलाइट रंग", + "clearHighlightColor": "हायलाइट काढा", + "customColor": "स्वतःचा रंग", + "hexValue": "Hex मूल्य", + "opacity": "अपारदर्शकता", + "resetToDefaultColor": "डीफॉल्ट रंगावर रीसेट करा", + "ltr": "LTR", + "rtl": "RTL", + "auto": "स्वयंचलित", + "cut": "कट", + "copy": "कॉपी", + "paste": "पेस्ट", + "find": "शोधा", + "select": "निवडा", + "selectAll": "सर्व निवडा", + "previousMatch": "मागील जुळणारे", + "nextMatch": "पुढील जुळणारे", + "closeFind": "बंद करा", + "replace": "बदला", + "replaceAll": "सर्व बदला", + "regex": "Regex", + "caseSensitive": "केस सेंसिटिव्ह", + "uploadImage": "प्रतिमा अपलोड करा", + "urlImage": "URL प्रतिमा", + "incorrectLink": "चुकीची लिंक", + "upload": "अपलोड", + "chooseImage": "प्रतिमा निवडा", + "loading": "लोड करत आहे", + "imageLoadFailed": "प्रतिमा लोड करण्यात अयशस्वी", + "divider": "विभाजक", + "table": "तक्त्याचे स्वरूप", + "colAddBefore": "यापूर्वी स्तंभ जोडा", + "rowAddBefore": "यापूर्वी पंक्ती जोडा", + "colAddAfter": "यानंतर स्तंभ जोडा", + "rowAddAfter": "यानंतर पंक्ती जोडा", + "colRemove": "स्तंभ काढा", + "rowRemove": "पंक्ती काढा", + "colDuplicate": "स्तंभ डुप्लिकेट", + "rowDuplicate": "पंक्ती डुप्लिकेट", + "colClear": "सामग्री साफ करा", + "rowClear": "सामग्री साफ करा", + "slashPlaceHolder": "'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा", + "typeSomething": "काहीतरी लिहा...", + "toggleListShortForm": "टॉगल", + "quoteListShortForm": "कोट", + "mathEquationShortForm": "सूत्र", + "codeBlockShortForm": "कोड" +}, + "favorite": { + "noFavorite": "कोणतेही आवडते पृष्ठ नाही", + "noFavoriteHintText": "पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा", + "removeFromSidebar": "साइडबारमधून काढा", + "addToSidebar": "साइडबारमध्ये पिन करा" +}, +"cardDetails": { + "notesPlaceholder": "/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा" +}, +"blockPlaceholders": { + "todoList": "करण्याची यादी", + "bulletList": "यादी", + "numberList": "क्रमांकित यादी", + "quote": "कोट", + "heading": "मथळा {}" +}, +"titleBar": { + "pageIcon": "पृष्ठ चिन्ह", + "language": "भाषा", + "font": "फॉन्ट", + "actions": "क्रिया", + "date": "तारीख", + "addField": "फील्ड जोडा", + "userIcon": "वापरकर्त्याचे चिन्ह" +}, +"noLogFiles": "कोणतीही लॉग फाइल्स नाहीत", +"newSettings": { + "myAccount": { + "title": "माझे खाते", + "subtitle": "तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.", + "profileLabel": "खाते नाव आणि प्रोफाइल चित्र", + "profileNamePlaceholder": "तुमचे नाव प्रविष्ट करा", + "accountSecurity": "खाते सुरक्षा", + "2FA": "2-स्टेप प्रमाणीकरण", + "aiKeys": "AI कीज", + "accountLogin": "खाते लॉगिन", + "updateNameError": "नाव अपडेट करण्यात अयशस्वी", + "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", + "aboutAppFlowy": "@:appName विषयी", + "deleteAccount": { + "title": "खाते हटवा", + "subtitle": "तुमचे खाते आणि सर्व डेटा कायमचे हटवा.", + "description": "तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.", + "deleteMyAccount": "माझे खाते हटवा", + "dialogTitle": "खाते हटवा", + "dialogContent1": "तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?", + "dialogContent2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.", + "confirmHint1": "कृपया पुष्टीसाठी \"@:newSettings.myAccount.deleteAccount.confirmHint3\" टाइप करा.", + "confirmHint2": "मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.", + "confirmHint3": "DELETE MY ACCOUNT", + "checkToConfirmError": "हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे", + "failedToGetCurrentUser": "वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी", + "confirmTextValidationFailed": "तुमचा पुष्टी मजकूर \"@:newSettings.myAccount.deleteAccount.confirmHint3\" शी जुळत नाही", + "deleteAccountSuccess": "खाते यशस्वीरित्या हटवले गेले" + } + }, + "workplace": { + "name": "वर्कस्पेस", + "title": "वर्कस्पेस सेटिंग्स", + "subtitle": "तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.", + "workplaceName": "वर्कस्पेसचे नाव", + "workplaceNamePlaceholder": "वर्कस्पेसचे नाव टाका", + "workplaceIcon": "वर्कस्पेस चिन्ह", + "workplaceIconSubtitle": "एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.", + "renameError": "वर्कस्पेसचे नाव बदलण्यात अयशस्वी", + "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", + "chooseAnIcon": "चिन्ह निवडा", + "appearance": { + "name": "दृश्यरूप", + "themeMode": { + "auto": "स्वयंचलित", + "light": "प्रकाश मोड", + "dark": "गडद मोड" + }, + "language": "भाषा" + } + }, + "syncState": { + "syncing": "सिंक्रोनायझ करत आहे", + "synced": "सिंक्रोनायझ झाले", + "noNetworkConnected": "नेटवर्क कनेक्ट केलेले नाही" + } +}, + "pageStyle": { + "title": "पृष्ठ शैली", + "layout": "लेआउट", + "coverImage": "मुखपृष्ठ प्रतिमा", + "pageIcon": "पृष्ठ चिन्ह", + "colors": "रंग", + "gradient": "ग्रेडियंट", + "backgroundImage": "पार्श्वभूमी प्रतिमा", + "presets": "पूर्वनियोजित", + "photo": "फोटो", + "unsplash": "Unsplash", + "pageCover": "पृष्ठ कव्हर", + "none": "काही नाही", + "openSettings": "सेटिंग्स उघडा", + "photoPermissionTitle": "@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे", + "photoPermissionDescription": "तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे", + "cameraPermissionTitle": "@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे", + "cameraPermissionDescription": "कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे", + "doNotAllow": "परवानगी देऊ नका", + "image": "प्रतिमा" +}, +"commandPalette": { + "placeholder": "शोधा किंवा प्रश्न विचारा...", + "bestMatches": "सर्वोत्तम जुळवणी", + "recentHistory": "अलीकडील इतिहास", + "navigateHint": "नेव्हिगेट करण्यासाठी", + "loadingTooltip": "आम्ही निकाल शोधत आहोत...", + "betaLabel": "बेटा", + "betaTooltip": "सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो", + "fromTrashHint": "कचरापेटीतून", + "noResultsHint": "आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.", + "clearSearchTooltip": "शोध फील्ड साफ करा" +}, +"space": { + "delete": "हटवा", + "deleteConfirmation": "हटवा: ", + "deleteConfirmationDescription": "या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.", + "rename": "स्पेसचे नाव बदला", + "changeIcon": "चिन्ह बदला", + "manage": "स्पेस व्यवस्थापित करा", + "addNewSpace": "स्पेस तयार करा", + "collapseAllSubPages": "सर्व उपपृष्ठे संकुचित करा", + "createNewSpace": "नवीन स्पेस तयार करा", + "createSpaceDescription": "तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.", + "spaceName": "स्पेसचे नाव", + "spaceNamePlaceholder": "उदा. मार्केटिंग, अभियांत्रिकी, HR", + "permission": "स्पेस परवानगी", + "publicPermission": "सार्वजनिक", + "publicPermissionDescription": "पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य", + "privatePermission": "खाजगी", + "privatePermissionDescription": "फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे", + "spaceIconBackground": "पार्श्वभूमीचा रंग", + "spaceIcon": "चिन्ह", + "dangerZone": "धोकादायक क्षेत्र", + "unableToDeleteLastSpace": "शेवटची स्पेस हटवता येणार नाही", + "unableToDeleteSpaceNotCreatedByYou": "तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही", + "enableSpacesForYourWorkspace": "तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा", + "title": "स्पेसेस", + "defaultSpaceName": "सामान्य", + "upgradeSpaceTitle": "स्पेस सक्षम करा", + "upgradeSpaceDescription": "तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.", + "upgrade": "अपग्रेड", + "upgradeYourSpace": "अनेक स्पेस तयार करा", + "quicklySwitch": "पुढील स्पेसवर पटकन स्विच करा", + "duplicate": "स्पेस डुप्लिकेट करा", + "movePageToSpace": "पृष्ठ स्पेसमध्ये हलवा", + "cannotMovePageToDatabase": "पृष्ठ डेटाबेसमध्ये हलवता येणार नाही", + "switchSpace": "स्पेस स्विच करा", + "spaceNameCannotBeEmpty": "स्पेसचे नाव रिकामे असू शकत नाही", + "success": { + "deleteSpace": "स्पेस यशस्वीरित्या हटवली", + "renameSpace": "स्पेसचे नाव यशस्वीरित्या बदलले", + "duplicateSpace": "स्पेस यशस्वीरित्या डुप्लिकेट केली", + "updateSpace": "स्पेस यशस्वीरित्या अपडेट केली" + }, + "error": { + "deleteSpace": "स्पेस हटवण्यात अयशस्वी", + "renameSpace": "स्पेसचे नाव बदलण्यात अयशस्वी", + "duplicateSpace": "स्पेस डुप्लिकेट करण्यात अयशस्वी", + "updateSpace": "स्पेस अपडेट करण्यात अयशस्वी" + }, + "createSpace": "स्पेस तयार करा", + "manageSpace": "स्पेस व्यवस्थापित करा", + "renameSpace": "स्पेसचे नाव बदला", + "mSpaceIconColor": "स्पेस चिन्हाचा रंग", + "mSpaceIcon": "स्पेस चिन्ह" +}, + "publish": { + "hasNotBeenPublished": "हे पृष्ठ अजून प्रकाशित केलेले नाही", + "spaceHasNotBeenPublished": "स्पेस प्रकाशित करण्यासाठी समर्थन नाही", + "reportPage": "पृष्ठाची तक्रार करा", + "databaseHasNotBeenPublished": "डेटाबेस प्रकाशित करण्यास समर्थन नाही.", + "createdWith": "यांनी तयार केले", + "downloadApp": "AppFlowy डाउनलोड करा", + "copy": { + "codeBlock": "कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे", + "imageBlock": "प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "mathBlock": "गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे", + "fileBlock": "फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे" + }, + "containsPublishedPage": "या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?", + "publishSuccessfully": "यशस्वीरित्या प्रकाशित झाले", + "unpublishSuccessfully": "यशस्वीरित्या अनपब्लिश झाले", + "publishFailed": "प्रकाशित करण्यात अयशस्वी", + "unpublishFailed": "अनपब्लिश करण्यात अयशस्वी", + "noAccessToVisit": "या पृष्ठावर प्रवेश नाही...", + "createWithAppFlowy": "AppFlowy ने वेबसाइट तयार करा", + "fastWithAI": "AI सह जलद आणि सोपे.", + "tryItNow": "आत्ताच वापरून पहा", + "onlyGridViewCanBePublished": "फक्त Grid view प्रकाशित केला जाऊ शकतो", + "database": { + "zero": "{} निवडलेले दृश्य प्रकाशित करा", + "one": "{} निवडलेली दृश्ये प्रकाशित करा", + "many": "{} निवडलेली दृश्ये प्रकाशित करा", + "other": "{} निवडलेली दृश्ये प्रकाशित करा" + }, + "mustSelectPrimaryDatabase": "प्राथमिक दृश्य निवडणे आवश्यक आहे", + "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.", + "unableToDeselectPrimaryDatabase": "प्राथमिक डेटाबेस अननिवड करता येणार नाही", + "saveThisPage": "या टेम्पलेटपासून सुरू करा", + "duplicateTitle": "तुम्हाला हे कुठे जोडायचे आहे", + "selectWorkspace": "वर्कस्पेस निवडा", + "addTo": "मध्ये जोडा", + "duplicateSuccessfully": "तुमच्या वर्कस्पेसमध्ये जोडले गेले", + "duplicateSuccessfullyDescription": "AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.", + "downloadIt": "डाउनलोड करा", + "openApp": "अ‍ॅपमध्ये उघडा", + "duplicateFailed": "डुप्लिकेट करण्यात अयशस्वी", + "membersCount": { + "zero": "सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "useThisTemplate": "हा टेम्पलेट वापरा" +}, +"web": { + "continue": "पुढे जा", + "or": "किंवा", + "continueWithGoogle": "Google सह पुढे जा", + "continueWithGithub": "GitHub सह पुढे जा", + "continueWithDiscord": "Discord सह पुढे जा", + "continueWithApple": "Apple सह पुढे जा", + "moreOptions": "अधिक पर्याय", + "collapse": "आकुंचन", + "signInAgreement": "\"पुढे जा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", + "signInLocalAgreement": "\"सुरुवात करा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", + "and": "आणि", + "termOfUse": "वापर अटी", + "privacyPolicy": "गोपनीयता धोरण", + "signInError": "साइन इन त्रुटी", + "login": "साइन अप किंवा लॉग इन करा", + "fileBlock": { + "uploadedAt": "{time} रोजी अपलोड केले", + "linkedAt": "{time} रोजी लिंक जोडली", + "empty": "फाईल अपलोड करा किंवा एम्बेड करा", + "uploadFailed": "अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "retry": "पुन्हा प्रयत्न करा" + }, + "importNotion": "Notion वरून आयात करा", + "import": "आयात करा", + "importSuccess": "यशस्वीरित्या अपलोड केले", + "importSuccessMessage": "आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.", + "importFailed": "आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा", + "dropNotionFile": "तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा", + "error": { + "pageNameIsEmpty": "पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा" + } +}, + "globalComment": { + "comments": "टिप्पण्या", + "addComment": "टिप्पणी जोडा", + "reactedBy": "यांनी प्रतिक्रिया दिली", + "addReaction": "प्रतिक्रिया जोडा", + "reactedByMore": "आणि {count} इतर", + "showSeconds": { + "one": "1 सेकंदापूर्वी", + "other": "{count} सेकंदांपूर्वी", + "zero": "आत्ताच", + "many": "{count} सेकंदांपूर्वी" + }, + "showMinutes": { + "one": "1 मिनिटापूर्वी", + "other": "{count} मिनिटांपूर्वी", + "many": "{count} मिनिटांपूर्वी" + }, + "showHours": { + "one": "1 तासापूर्वी", + "other": "{count} तासांपूर्वी", + "many": "{count} तासांपूर्वी" + }, + "showDays": { + "one": "1 दिवसापूर्वी", + "other": "{count} दिवसांपूर्वी", + "many": "{count} दिवसांपूर्वी" + }, + "showMonths": { + "one": "1 महिन्यापूर्वी", + "other": "{count} महिन्यांपूर्वी", + "many": "{count} महिन्यांपूर्वी" + }, + "showYears": { + "one": "1 वर्षापूर्वी", + "other": "{count} वर्षांपूर्वी", + "many": "{count} वर्षांपूर्वी" + }, + "reply": "उत्तर द्या", + "deleteComment": "टिप्पणी हटवा", + "youAreNotOwner": "तुम्ही या टिप्पणीचे मालक नाही", + "confirmDeleteDescription": "तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?", + "hasBeenDeleted": "हटवले गेले", + "replyingTo": "याला उत्तर देत आहे", + "noAccessDeleteComment": "तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही", + "collapse": "संकुचित करा", + "readMore": "अधिक वाचा", + "failedToAddComment": "टिप्पणी जोडण्यात अयशस्वी", + "commentAddedSuccessfully": "टिप्पणी यशस्वीरित्या जोडली गेली.", + "commentAddedSuccessTip": "तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?" +}, + "template": { + "asTemplate": "टेम्पलेट म्हणून जतन करा", + "name": "टेम्पलेट नाव", + "description": "टेम्पलेट वर्णन", + "about": "टेम्पलेट माहिती", + "deleteFromTemplate": "टेम्पलेटमधून हटवा", + "preview": "टेम्पलेट पूर्वदृश्य", + "categories": "टेम्पलेट श्रेणी", + "isNewTemplate": "नवीन टेम्पलेटमध्ये पिन करा", + "featured": "वैशिष्ट्यीकृतमध्ये पिन करा", + "relatedTemplates": "संबंधित टेम्पलेट्स", + "requiredField": "{field} आवश्यक आहे", + "addCategory": "\"{category}\" जोडा", + "addNewCategory": "नवीन श्रेणी जोडा", + "addNewCreator": "नवीन निर्माता जोडा", + "deleteCategory": "श्रेणी हटवा", + "editCategory": "श्रेणी संपादित करा", + "editCreator": "निर्माता संपादित करा", + "category": { + "name": "श्रेणीचे नाव", + "icon": "श्रेणी चिन्ह", + "bgColor": "श्रेणी पार्श्वभूमीचा रंग", + "priority": "श्रेणी प्राधान्य", + "desc": "श्रेणीचे वर्णन", + "type": "श्रेणी प्रकार", + "icons": "श्रेणी चिन्हे", + "colors": "श्रेणी रंग", + "byUseCase": "वापराच्या आधारे", + "byFeature": "वैशिष्ट्यांनुसार", + "deleteCategory": "श्रेणी हटवा", + "deleteCategoryDescription": "तुम्हाला ही श्रेणी हटवायची आहे का?", + "typeToSearch": "श्रेणी शोधण्यासाठी टाइप करा..." + }, + "creator": { + "label": "टेम्पलेट निर्माता", + "name": "निर्मात्याचे नाव", + "avatar": "निर्मात्याचा अवतार", + "accountLinks": "निर्मात्याचे खाते दुवे", + "uploadAvatar": "अवतार अपलोड करण्यासाठी क्लिक करा", + "deleteCreator": "निर्माता हटवा", + "deleteCreatorDescription": "तुम्हाला हा निर्माता हटवायचा आहे का?", + "typeToSearch": "निर्माते शोधण्यासाठी टाइप करा..." + }, + "uploadSuccess": "टेम्पलेट यशस्वीरित्या अपलोड झाले", + "uploadSuccessDescription": "तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.", + "viewTemplate": "टेम्पलेट पहा", + "deleteTemplate": "टेम्पलेट हटवा", + "deleteSuccess": "टेम्पलेट यशस्वीरित्या हटवले गेले", + "deleteTemplateDescription": "याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?", + "addRelatedTemplate": "संबंधित टेम्पलेट जोडा", + "removeRelatedTemplate": "संबंधित टेम्पलेट हटवा", + "uploadAvatar": "अवतार अपलोड करा", + "searchInCategory": "{category} मध्ये शोधा", + "label": "टेम्पलेट्स" +}, + "fileDropzone": { + "dropFile": "फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा", + "uploading": "अपलोड करत आहे...", + "uploadFailed": "अपलोड अयशस्वी", + "uploadSuccess": "अपलोड यशस्वी", + "uploadSuccessDescription": "फाइल यशस्वीरित्या अपलोड झाली आहे", + "uploadFailedDescription": "फाइल अपलोड अयशस्वी झाली आहे", + "uploadingDescription": "फाइल अपलोड होत आहे" +}, + "gallery": { + "preview": "पूर्ण स्क्रीनमध्ये उघडा", + "copy": "कॉपी करा", + "download": "डाउनलोड", + "prev": "मागील", + "next": "पुढील", + "resetZoom": "झूम रिसेट करा", + "zoomIn": "झूम इन", + "zoomOut": "झूम आउट" +}, + "invitation": { + "join": "सामील व्हा", + "on": "वर", + "invitedBy": "यांनी आमंत्रित केले", + "membersCount": { + "zero": "{count} सदस्य", + "one": "{count} सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "tip": "तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.", + "joinWorkspace": "वर्कस्पेसमध्ये सामील व्हा", + "success": "तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात", + "successMessage": "आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.", + "openWorkspace": "AppFlowy उघडा", + "alreadyAccepted": "तुम्ही आधीच आमंत्रण स्वीकारले आहे", + "errorModal": { + "title": "काहीतरी चुकले आहे", + "description": "तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.", + "contactOwner": "मालकाशी संपर्क करा", + "close": "मुख्यपृष्ठावर परत जा", + "changeAccount": "खाते बदला" + } +}, + "requestAccess": { + "title": "या पृष्ठासाठी प्रवेश नाही", + "subtitle": "तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.", + "requestAccess": "प्रवेशाची विनंती करा", + "backToHome": "मुख्यपृष्ठावर परत जा", + "tip": "तुम्ही सध्या म्हणून लॉग इन आहात.", + "mightBe": "कदाचित तुम्हाला दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.", + "successful": "विनंती यशस्वीपणे पाठवली गेली", + "successfulMessage": "मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.", + "requestError": "प्रवेशाची विनंती अयशस्वी", + "repeatRequestError": "तुम्ही यासाठी आधीच विनंती केली आहे" +}, + "approveAccess": { + "title": "वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा", + "requestSummary": " यांनी मध्ये सामील होण्यासाठी आणि पाहण्यासाठी विनंती केली आहे", + "upgrade": "अपग्रेड", + "downloadApp": "AppFlowy डाउनलोड करा", + "approveButton": "मंजूर करा", + "approveSuccess": "मंजूर यशस्वी", + "approveError": "मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा", + "getRequestInfoError": "विनंतीची माहिती मिळवण्यात अयशस्वी", + "memberCount": { + "zero": "कोणतेही सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "alreadyProTitle": "वर्कस्पेस योजना मर्यादा गाठली आहे", + "alreadyProMessage": "अधिक सदस्यांसाठी शी संपर्क साधा", + "repeatApproveError": "तुम्ही ही विनंती आधीच मंजूर केली आहे", + "ensurePlanLimit": "कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना करा किंवा करा.", + "requestToJoin": "मध्ये सामील होण्यासाठी विनंती केली", + "asMember": "सदस्य म्हणून" +}, + "upgradePlanModal": { + "title": "Pro प्लॅनवर अपग्रेड करा", + "message": "{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.", + "upgradeSteps": "AppFlowy वर तुमची योजना कशी अपग्रेड करावी:", + "step1": "1. सेटिंग्जमध्ये जा", + "step2": "2. 'योजना' वर क्लिक करा", + "step3": "3. 'योजना बदला' निवडा", + "appNote": "नोंद:", + "actionButton": "अपग्रेड करा", + "downloadLink": "अ‍ॅप डाउनलोड करा", + "laterButton": "नंतर", + "refreshNote": "यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी वर क्लिक करा.", + "refresh": "येथे" +}, + "breadcrumbs": { + "label": "ब्रेडक्रम्स" +}, + "time": { + "justNow": "आत्ताच", + "seconds": { + "one": "1 सेकंद", + "other": "{count} सेकंद" + }, + "minutes": { + "one": "1 मिनिट", + "other": "{count} मिनिटे" + }, + "hours": { + "one": "1 तास", + "other": "{count} तास" + }, + "days": { + "one": "1 दिवस", + "other": "{count} दिवस" + }, + "weeks": { + "one": "1 आठवडा", + "other": "{count} आठवडे" + }, + "months": { + "one": "1 महिना", + "other": "{count} महिने" + }, + "years": { + "one": "1 वर्ष", + "other": "{count} वर्षे" + }, + "ago": "पूर्वी", + "yesterday": "काल", + "today": "आज" +}, + "members": { + "zero": "सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" +}, + "tabMenu": { + "close": "बंद करा", + "closeDisabledHint": "पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा", + "closeOthers": "इतर टॅब बंद करा", + "closeOthersHint": "हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता", + "closeOthersDisabledHint": "सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत", + "favorite": "आवडते", + "unfavorite": "आवडते काढा", + "favoriteDisabledHint": "हे दृश्य आवडते म्हणून जतन करता येत नाही", + "pinTab": "पिन करा", + "unpinTab": "अनपिन करा" +}, + "openFileMessage": { + "success": "फाइल यशस्वीरित्या उघडली", + "fileNotFound": "फाइल सापडली नाही", + "noAppToOpenFile": "ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही", + "permissionDenied": "ही फाइल उघडण्यासाठी परवानगी नाही", + "unknownError": "फाइल उघडण्यात अयशस्वी" +}, + "inviteMember": { + "requestInviteMembers": "तुमच्या वर्कस्पेसमध्ये आमंत्रित करा", + "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, कृपया ", + "upgrade": "अपग्रेड करा", + "addEmail": "email@example.com, email2@example.com...", + "requestInvites": "आमंत्रण पाठवा", + "inviteAlready": "तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}", + "inviteSuccess": "आमंत्रण यशस्वीपणे पाठवले", + "description": "खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.", + "emails": "ईमेल" +}, + "quickNote": { + "label": "झटपट नोंद", + "quickNotes": "झटपट नोंदी", + "search": "झटपट नोंदी शोधा", + "collapseFullView": "पूर्ण दृश्य लपवा", + "expandFullView": "पूर्ण दृश्य उघडा", + "createFailed": "झटपट नोंद तयार करण्यात अयशस्वी", + "quickNotesEmpty": "कोणत्याही झटपट नोंदी नाहीत", + "emptyNote": "रिकामी नोंद", + "deleteNotePrompt": "निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?", + "addNote": "नवीन नोंद", + "noAdditionalText": "अधिक माहिती नाही" +}, + "subscribe": { + "upgradePlanTitle": "योजना तुलना करा आणि निवडा", + "yearly": "वार्षिक", + "save": "{discount}% बचत", + "monthly": "मासिक", + "priceIn": "किंमत येथे: ", + "free": "फ्री", + "pro": "प्रो", + "freeDescription": "2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी", + "proDescription": "प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी", + "proDuration": { + "monthly": "प्रति सदस्य प्रति महिना\nमासिक बिलिंग", + "yearly": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग" + }, + "cancel": "खालच्या योजनेवर जा", + "changePlan": "प्रो योजनेवर अपग्रेड करा", + "everythingInFree": "फ्री योजनेतील सर्व काही +", + "currentPlan": "सध्याची योजना", + "freeDuration": "कायम", + "freePoints": { + "first": "1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)", + "second": "अमर्यादित पृष्ठे आणि ब्लॉक्स", + "three": "5 GB संचयन", + "four": "बुद्धिमान शोध", + "five": "20 AI प्रतिसाद", + "six": "मोबाईल अ‍ॅप", + "seven": "रिअल-टाइम सहकार्य" + }, + "proPoints": { + "first": "अमर्यादित संचयन", + "second": "10 वर्कस्पेस सदस्यांपर्यंत", + "three": "अमर्यादित AI प्रतिसाद", + "four": "अमर्यादित फाइल अपलोड्स", + "five": "कस्टम नेमस्पेस" + }, + "cancelPlan": { + "title": "आपल्याला जाताना पाहून वाईट वाटते", + "success": "आपली सदस्यता यशस्वीरित्या रद्द झाली आहे", + "description": "आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.", + "commonOther": "इतर", + "otherHint": "आपले उत्तर येथे लिहा", + "questionOne": { + "question": "तुम्ही AppFlowy Pro सदस्यता का रद्द केली?", + "answerOne": "खर्च खूप जास्त आहे", + "answerTwo": "वैशिष्ट्ये अपेक्षेनुसार नव्हती", + "answerThree": "चांगला पर्याय सापडला", + "answerFour": "खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता", + "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" + }, + "questionTwo": { + "question": "तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?", + "answerOne": "खूप शक्यता आहे", + "answerTwo": "काहीशी शक्यता आहे", + "answerThree": "निश्चित नाही", + "answerFour": "अल्प शक्यता आहे", + "answerFive": "शक्यता नाही" + }, + "questionThree": { + "question": "सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?", + "answerOne": "मल्टी-यूजर सहकार्य", + "answerTwo": "लांब कालावधीसाठी आवृत्ती इतिहास", + "answerThree": "अमर्यादित AI प्रतिसाद", + "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" + }, + "questionFour": { + "question": "AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?", + "answerOne": "छान", + "answerTwo": "चांगला", + "answerThree": "सामान्य", + "answerFour": "थोडासा वाईट", + "answerFive": "असंतोषजनक" + } + } +}, + "ai": { + "contentPolicyViolation": "संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.", + "textLimitReachedDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.", + "imageLimitReachedDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.", + "limitReachedAction": { + "textDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया", + "imageDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया", + "upgrade": "अपग्रेड करा", + "toThe": "या योजनेवर", + "proPlan": "प्रो योजना", + "orPurchaseAn": "किंवा खरेदी करा", + "aiAddon": "AI अ‍ॅड-ऑन" + }, + "editing": "संपादन करत आहे", + "analyzing": "विश्लेषण करत आहे", + "continueWritingEmptyDocumentTitle": "लेखन सुरू ठेवता आले नाही", + "continueWritingEmptyDocumentDescription": "तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!", + "more": "अधिक" +}, + "autoUpdate": { + "criticalUpdateTitle": "अद्यतन आवश्यक आहे", + "criticalUpdateDescription": "तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.", + "criticalUpdateButton": "अद्यतन करा", + "bannerUpdateTitle": "नवीन आवृत्ती उपलब्ध!", + "bannerUpdateDescription": "नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.", + "bannerUpdateButton": "अद्यतन करा", + "settingsUpdateTitle": "नवीन आवृत्ती ({newVersion}) उपलब्ध!", + "settingsUpdateDescription": "सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}", + "settingsUpdateButton": "अद्यतन करा", + "settingsUpdateWhatsNew": "काय नवीन आहे" +}, + "lockPage": { + "lockPage": "लॉक केलेले", + "reLockPage": "पुन्हा लॉक करा", + "lockTooltip": "अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.", + "pageLockedToast": "पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.", + "lockedOperationTooltip": "पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे." +}, + "suggestion": { + "accept": "स्वीकारा", + "keep": "जसे आहे तसे ठेवा", + "discard": "रद्द करा", + "close": "बंद करा", + "tryAgain": "पुन्हा प्रयत्न करा", + "rewrite": "पुन्हा लिहा", + "insertBelow": "खाली टाका" +} +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 2be124e2fe..ce28ea68b0 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -98,6 +98,7 @@ class InitAppWidgetTask extends LaunchTask { Locale('zh', 'TW'), Locale('fa'), Locale('hin'), + Locale('mr','IN'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 0b6ff4fb3f..5ad2435e99 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -48,6 +48,8 @@ String languageFromLocale(Locale locale) { default: return locale.languageCode; } + case "mr": + return "मराठी"; case "he": return "עברית"; case "hu": @@ -79,7 +81,7 @@ String languageFromLocale(Locale locale) { case "ur": return "اردو"; case "hin": - return "हिन्दी"; + return "हिन्दी"; } // If not found then the language code will be displayed return locale.languageCode; diff --git a/frontend/resources/translations/mr-IN.json b/frontend/resources/translations/mr-IN.json new file mode 100644 index 0000000000..f86a1e0081 --- /dev/null +++ b/frontend/resources/translations/mr-IN.json @@ -0,0 +1,3210 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "मी", + "welcomeText": "@:appName मध्ये आ पले स्वागत आहे.", + "welcomeTo": "मध्ये आ पले स्वागत आ हे", + "githubStarText": "GitHub वर स्टार करा", + "subscribeNewsletterText": "वृत्तपत्राची सदस्यता घ्या", + "letsGoButtonText": "क्विक स्टार्ट", + "title": "Title", + "youCanAlso": "तुम्ही देखील", + "and": "आ णि", + "failedToOpenUrl": "URL उघडण्यात अयशस्वी: {}", + "blockActions": { + "addBelowTooltip": "खाली जोडण्यासाठी क्लिक करा", + "addAboveCmd": "Alt+click", + "addAboveMacCmd": "Option+click", + "addAboveTooltip": "वर जोडण्यासाठी", + "dragTooltip": "Drag to move", + "openMenuTooltip": "मेनू उघडण्यासाठी क्लिक करा" + }, + "signUp": { + "buttonText": "साइन अप", + "title": "साइन अप to @:appName", + "getStartedText": "सुरुवात करा", + "emptyPasswordError": "पासवर्ड रिकामा असू शकत नाही", + "repeatPasswordEmptyError": "Repeat पासवर्ड रिकामा असू शकत नाही", + "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", + "alreadyHaveAnAccount": "आधीच खाते आहे?", + "emailHint": "Email", + "passwordHint": "Password", + "repeatPasswordHint": "पासवर्ड पुन्हा लिहा", + "signUpWith": "यामध्ये साइन अप करा:" + }, + "signIn": { + "loginTitle": "@:appName मध्ये लॉगिन करा", + "loginButtonText": "लॉगिन", + "loginStartWithAnonymous": "अनामिक सत्रासह पुढे जा", + "continueAnonymousUser": "अनामिक सत्रासह पुढे जा", + "anonymous": "अनामिक", + "buttonText": "साइन इन", + "signingInText": "साइन इन होत आहे...", + "forgotPassword": "पासवर्ड विसरलात?", + "emailHint": "ईमेल", + "passwordHint": "पासवर्ड", + "dontHaveAnAccount": "तुमचं खाते नाही?", + "createAccount": "खाते तयार करा", + "repeatPasswordEmptyError": "पुन्हा पासवर्ड रिकामा असू शकत नाही", + "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", + "syncPromptMessage": "डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका", + "or": "किंवा", + "signInWithGoogle": "Google सह पुढे जा", + "signInWithGithub": "GitHub सह पुढे जा", + "signInWithDiscord": "Discord सह पुढे जा", + "signInWithApple": "Apple सह पुढे जा", + "continueAnotherWay": "इतर पर्यायांनी पुढे जा", + "signUpWithGoogle": "Google सह साइन अप करा", + "signUpWithGithub": "GitHub सह साइन अप करा", + "signUpWithDiscord": "Discord सह साइन अप करा", + "signInWith": "यासह पुढे जा:", + "signInWithEmail": "ईमेलसह पुढे जा", + "signInWithMagicLink": "पुढे जा", + "signUpWithMagicLink": "Magic Link सह साइन अप करा", + "pleaseInputYourEmail": "कृपया तुमचा ईमेल पत्ता टाका", + "settings": "सेटिंग्ज", + "magicLinkSent": "Magic Link पाठवण्यात आली आहे!", + "invalidEmail": "कृपया वैध ईमेल पत्ता टाका", + "alreadyHaveAnAccount": "आधीच खाते आहे?", + "logIn": "लॉगिन", + "generalError": "काहीतरी चुकलं. कृपया नंतर प्रयत्न करा", + "limitRateError": "सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता", + "magicLinkSentDescription": "तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल." + }, + "workspace": { + "chooseWorkspace": "तुमचे workspace निवडा", + "defaultName": "माझे Workspace", + "create": "नवीन workspace तयार करा", + "new": "नवीन workspace", + "importFromNotion": "Notion मधून आयात करा", + "learnMore": "अधिक जाणून घ्या", + "reset": "workspace रीसेट करा", + "renameWorkspace": "workspace चे नाव बदला", + "workspaceNameCannotBeEmpty": "workspace चे नाव रिकामे असू शकत नाही", + "resetWorkspacePrompt": "workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.", + "hint": "workspace", + "notFoundError": "workspace सापडले नाही", + "failedToLoad": "काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.", + "errorActions": { + "reportIssue": "समस्या नोंदवा", + "reportIssueOnGithub": "Github वर समस्या नोंदवा", + "exportLogFiles": "लॉग फाइल्स निर्यात करा", + "reachOut": "Discord वर संपर्क करा" + }, + "menuTitle": "कार्यक्षेत्रे", + "deleteWorkspaceHintText": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.", + "createSuccess": "कार्यक्षेत्र यशस्वीरित्या तयार झाले", + "createFailed": "कार्यक्षेत्र तयार करण्यात अयशस्वी", + "createLimitExceeded": "तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.", + "deleteSuccess": "कार्यक्षेत्र यशस्वीरित्या हटवले गेले", + "deleteFailed": "कार्यक्षेत्र हटवण्यात अयशस्वी", + "openSuccess": "कार्यक्षेत्र यशस्वीरित्या उघडले", + "openFailed": "कार्यक्षेत्र उघडण्यात अयशस्वी", + "renameSuccess": "कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले", + "renameFailed": "कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी", + "updateIconSuccess": "कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले", + "updateIconFailed": "कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी", + "cannotDeleteTheOnlyWorkspace": "फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही", + "fetchWorkspacesFailed": "कार्यक्षेत्रे मिळवण्यात अयशस्वी", + "leaveCurrentWorkspace": "कार्यक्षेत्र सोडा", + "leaveCurrentWorkspacePrompt": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?" + }, + "shareAction": { + "buttonText": "शेअर करा", + "workInProgress": "लवकरच येत आहे", + "markdown": "Markdown", + "html": "HTML", + "clipboard": "क्लिपबोर्डवर कॉपी करा", + "csv": "CSV", + "copyLink": "लिंक कॉपी करा", + "publishToTheWeb": "वेबवर प्रकाशित करा", + "publishToTheWebHint": "AppFlowy सह वेबसाइट तयार करा", + "publish": "प्रकाशित करा", + "unPublish": "अप्रकाशित करा", + "visitSite": "साइटला भेट द्या", + "exportAsTab": "या स्वरूपात निर्यात करा", + "publishTab": "प्रकाशित करा", + "shareTab": "शेअर करा", + "publishOnAppFlowy": "AppFlowy वर प्रकाशित करा", + "shareTabTitle": "सहकार्य करण्यासाठी आमंत्रित करा", + "shareTabDescription": "कोणासही सहज सहकार्य करण्यासाठी", + "copyLinkSuccess": "लिंक क्लिपबोर्डवर कॉपी केली", + "copyShareLink": "शेअर लिंक कॉपी करा", + "copyLinkFailed": "लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", + "copyLinkToBlockSuccess": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली", + "copyLinkToBlockFailed": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", + "manageAllSites": "सर्व साइट्स व्यवस्थापित करा", + "updatePathName": "पथाचे नाव अपडेट करा" + }, + "moreAction": { + "small": "लहान", + "medium": "मध्यम", + "large": "मोठा", + "fontSize": "फॉन्ट आकार", + "import": "Import", + "moreOptions": "अधिक पर्याय", + "wordCount": "शब्द संख्या: {}", + "charCount": "अक्षर संख्या: {}", + "createdAt": "निर्मिती: {}", + "deleteView": "हटवा", + "duplicateView": "प्रत बनवा", + "wordCountLabel": "शब्द संख्या: ", + "charCountLabel": "अक्षर संख्या: ", + "createdAtLabel": "निर्मिती: ", + "syncedAtLabel": "सिंक केले: ", + "saveAsNewPage": "संदेश पृष्ठात जोडा", + "saveAsNewPageDisabled": "उपलब्ध संदेश नाहीत" + }, + "importPanel": { + "textAndMarkdown": "मजकूर आणि Markdown", + "documentFromV010": "v0.1.0 पासून दस्तऐवज", + "databaseFromV010": "v0.1.0 पासून डेटाबेस", + "notionZip": "Notion निर्यात केलेली Zip फाईल", + "csv": "CSV", + "database": "डेटाबेस" + }, + "emojiIconPicker": { + "iconUploader": { + "placeholderLeft": "फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा ", + "placeholderUpload": "अपलोड", + "placeholderRight": ", किंवा इमेज लिंक पेस्ट करा.", + "dropToUpload": "अपलोडसाठी फाईल ड्रॉप करा", + "change": "बदला" + } + }, + "disclosureAction": { + "rename": "नाव बदला", + "delete": "हटवा", + "duplicate": "प्रत बनवा", + "unfavorite": "आवडतीतून काढा", + "favorite": "आवडतीत जोडा", + "openNewTab": "नवीन टॅबमध्ये उघडा", + "moveTo": "या ठिकाणी हलवा", + "addToFavorites": "आवडतीत जोडा", + "copyLink": "लिंक कॉपी करा", + "changeIcon": "आयकॉन बदला", + "collapseAllPages": "सर्व उपपृष्ठे संकुचित करा", + "movePageTo": "पृष्ठ हलवा", + "move": "हलवा", + "lockPage": "पृष्ठ लॉक करा" + }, + "blankPageTitle": "रिक्त पृष्ठ", + "newPageText": "नवीन पृष्ठ", + "newDocumentText": "नवीन दस्तऐवज", + "newGridText": "नवीन ग्रिड", + "newCalendarText": "नवीन कॅलेंडर", + "newBoardText": "नवीन बोर्ड", + "chat": { + "newChat": "AI गप्पा", + "inputMessageHint": "@:appName AI ला विचार करा", + "inputLocalAIMessageHint": "@:appName लोकल AI ला विचार करा", + "unsupportedCloudPrompt": "ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे", + "relatedQuestion": "सूचवलेले", + "serverUnavailable": "कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा", + "aiServerUnavailable": "AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.", + "retry": "पुन्हा प्रयत्न करा", + "clickToRetry": "पुन्हा प्रयत्न करण्यासाठी क्लिक करा", + "regenerateAnswer": "उत्तर पुन्हा तयार करा", + "question1": "Kanban वापरून कामे कशी व्यवस्थापित करायची", + "question2": "GTD पद्धत समजावून सांगा", + "question3": "Rust का वापरावा", + "question4": "माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी", + "question5": "माझ्या पृष्ठासाठी एक चित्र तयार करा", + "question6": "या आठवड्याची माझी कामांची यादी तयार करा", + "aiMistakePrompt": "AI चुकू शकतो. महत्त्वाची माहिती तपासा.", + "chatWithFilePrompt": "तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?", + "indexFileSuccess": "फाईल यशस्वीरित्या अनुक्रमित केली गेली", + "inputActionNoPages": "काहीही पृष्ठे सापडली नाहीत", + "referenceSource": { + "zero": "0 स्रोत सापडले", + "one": "{count} स्रोत सापडला", + "other": "{count} स्रोत सापडले" + } + }, + "clickToMention": "पृष्ठाचा उल्लेख करा", + "uploadFile": "PDFs, मजकूर किंवा markdown फाइल्स जोडा", + "questionDetail": "नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?", + "indexingFile": "{} अनुक्रमित करत आहे", + "generatingResponse": "उत्तर तयार होत आहे", + "selectSources": "स्रोत निवडा", + "currentPage": "सध्याचे पृष्ठ", + "sourcesLimitReached": "तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता", + "sourceUnsupported": "सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही", + "regenerate": "पुन्हा प्रयत्न करा", + "addToPageButton": "संदेश पृष्ठावर जोडा", + "addToPageTitle": "या पृष्ठात संदेश जोडा...", + "addToNewPage": "नवीन पृष्ठ तयार करा", + "addToNewPageName": "\"{}\" मधून काढलेले संदेश", + "addToNewPageSuccessToast": "संदेश जोडण्यात आला", + "openPagePreviewFailedToast": "पृष्ठ उघडण्यात अयशस्वी", + "changeFormat": { + "actionButton": "फॉरमॅट बदला", + "confirmButton": "या फॉरमॅटसह पुन्हा तयार करा", + "textOnly": "मजकूर", + "imageOnly": "फक्त प्रतिमा", + "textAndImage": "मजकूर आणि प्रतिमा", + "text": "परिच्छेद", + "bullet": "बुलेट यादी", + "number": "क्रमांकित यादी", + "table": "सारणी", + "blankDescription": "उत्तराचे फॉरमॅट ठरवा", + "defaultDescription": "स्वयंचलित उत्तर फॉरमॅट", + "textWithImageDescription": "@:chat.changeFormat.text प्रतिमेसह", + "numberWithImageDescription": "@:chat.changeFormat.number प्रतिमेसह", + "bulletWithImageDescription": "@:chat.changeFormat.bullet प्रतिमेसह", + " tableWithImageDescription": "@:chat.changeFormat.table प्रतिमेसह" + }, + "switchModel": { + "label": "मॉडेल बदला", + "localModel": "स्थानिक मॉडेल", + "cloudModel": "क्लाऊड मॉडेल", + "autoModel": "स्वयंचलित" + }, + "selectBanner": { + "saveButton": "… मध्ये जोडा", + "selectMessages": "संदेश निवडा", + "nSelected": "{} निवडले गेले", + "allSelected": "सर्व निवडले गेले" + }, + "stopTooltip": "उत्पन्न करणे थांबवा", + "trash": { + "text": "कचरा", + "restoreAll": "सर्व पुनर्संचयित करा", + "restore": "पुनर्संचयित करा", + "deleteAll": "सर्व हटवा", + "pageHeader": { + "fileName": "फाईलचे नाव", + "lastModified": "शेवटचा बदल", + "created": "निर्मिती" + } + }, + "confirmDeleteAll": { + "title": "कचरापेटीतील सर्व पृष्ठे", + "caption": "तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "confirmRestoreAll": { + "title": "कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा", + "caption": "ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "restorePage": { + "title": "पुनर्संचयित करा: {}", + "caption": "तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?" + }, + "mobile": { + "actions": "कचरा क्रिया", + "empty": "कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत", + "emptyDescription": "जे आवश्यक नाही ते कचरापेटीत हलवा.", + "isDeleted": "हटवले गेले आहे", + "isRestored": "पुनर्संचयित केले गेले आहे" + }, + "confirmDeleteTitle": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?", + "deletePagePrompt": { + "text": "हे पृष्ठ कचरापेटीत आहे", + "restore": "पृष्ठ पुनर्संचयित करा", + "deletePermanent": "कायमचे हटवा", + "deletePermanentDescription": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." + }, + "dialogCreatePageNameHint": "पृष्ठाचे नाव", + "questionBubble": { + "shortcuts": "शॉर्टकट्स", + "whatsNew": "नवीन काय आहे?", + "help": "मदत आणि समर्थन", + "markdown": "Markdown", + "debug": { + "name": "डीबग माहिती", + "success": "डीबग माहिती क्लिपबोर्डवर कॉपी केली!", + "fail": "डीबग माहिती कॉपी करता आली नाही" + }, + "feedback": "अभिप्राय" + }, + "menuAppHeader": { + "moreButtonToolTip": "हटवा, नाव बदला आणि अधिक...", + "addPageTooltip": "तत्काळ एक पृष्ठ जोडा", + "defaultNewPageName": "शीर्षक नसलेले", + "renameDialog": "नाव बदला", + "pageNameSuffix": "प्रत" + }, + "noPagesInside": "अंदर कोणतीही पृष्ठे नाहीत", + "toolbar": { + "undo": "पूर्ववत करा", + "redo": "पुन्हा करा", + "bold": "ठळक", + "italic": "तिरकस", + "underline": "अधोरेखित", + "strike": "मागे ओढलेले", + "numList": "क्रमांकित यादी", + "bulletList": "बुलेट यादी", + "checkList": "चेक यादी", + "inlineCode": "इनलाइन कोड", + "quote": "उद्धरण ब्लॉक", + "header": "शीर्षक", + "highlight": "हायलाइट", + "color": "रंग", + "addLink": "लिंक जोडा" + }, + "tooltip": { + "lightMode": "लाइट मोडमध्ये स्विच करा", + "darkMode": "डार्क मोडमध्ये स्विच करा", + "openAsPage": "पृष्ठ म्हणून उघडा", + "addNewRow": "नवीन पंक्ती जोडा", + "openMenu": "मेनू उघडण्यासाठी क्लिक करा", + "dragRow": "पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा", + "viewDataBase": "डेटाबेस पहा", + "referencePage": "हे {name} संदर्भित आहे", + "addBlockBelow": "खाली एक ब्लॉक जोडा", + "aiGenerate": "निर्मिती करा" + }, + "sideBar": { + "closeSidebar": "साइडबार बंद करा", + "openSidebar": "साइडबार उघडा", + "expandSidebar": "पूर्ण पृष्ठावर विस्तारित करा", + "personal": "वैयक्तिक", + "private": "खाजगी", + "workspace": "कार्यक्षेत्र", + "favorites": "आवडती", + "clickToHidePrivate": "खाजगी जागा लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील", + "clickToHideWorkspace": "कार्यक्षेत्र लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील", + "clickToHidePersonal": "वैयक्तिक जागा लपवण्यासाठी क्लिक करा", + "clickToHideFavorites": "आवडती जागा लपवण्यासाठी क्लिक करा", + "addAPage": "नवीन पृष्ठ जोडा", + "addAPageToPrivate": "खाजगी जागेत पृष्ठ जोडा", + "addAPageToWorkspace": "कार्यक्षेत्रात पृष्ठ जोडा", + "recent": "अलीकडील", + "today": "आज", + "thisWeek": "या आठवड्यात", + "others": "पूर्वीच्या आवडती", + "earlier": "पूर्वीचे", + "justNow": "आत्ताच", + "minutesAgo": "{count} मिनिटांपूर्वी", + "lastViewed": "शेवटी पाहिलेले", + "favoriteAt": "आवडते म्हणून चिन्हांकित", + "emptyRecent": "अलीकडील पृष्ठे नाहीत", + "emptyRecentDescription": "तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.", + "emptyFavorite": "आवडती पृष्ठे नाहीत", + "emptyFavoriteDescription": "पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!", + "removePageFromRecent": "हे पृष्ठ अलीकडील यादीतून काढायचे?", + "removeSuccess": "यशस्वीरित्या काढले गेले", + "favoriteSpace": "आवडती", + "RecentSpace": "अलीकडील", + "Spaces": "जागा", + "upgradeToPro": "Pro मध्ये अपग्रेड करा", + "upgradeToAIMax": "अमर्यादित AI अनलॉक करा", + "storageLimitDialogTitle": "तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा", + "storageLimitDialogTitleIOS": "तुमचा मोफत स्टोरेज संपला आहे.", + "aiResponseLimitTitle": "तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा", + "aiResponseLimitDialogTitle": "AI प्रतिसाद मर्यादा गाठली आहे", + "aiResponseLimit": "तुमचे मोफत AI प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा", + "askOwnerToUpgradeToPro": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा", + "askOwnerToUpgradeToProIOS": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.", + "askOwnerToUpgradeToAIMax": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा", + "askOwnerToUpgradeToAIMaxIOS": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.", + "purchaseAIMax": "तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा", + "aiImageResponseLimit": "तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा", + "purchaseStorageSpace": "स्टोरेज स्पेस खरेदी करा", + "singleFileProPlanLimitationDescription": "तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा", + "purchaseAIResponse": "AI प्रतिसाद खरेदी करा", + "askOwnerToUpgradeToLocalAI": "कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा", + "upgradeToAILocal": "अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा", + "upgradeToAILocalDesc": "PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा" +}, + "notifications": { + "export": { + "markdown": "टीप Markdown मध्ये निर्यात केली", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "संपर्क", + "whatsHappening": "या आठवड्यात काय घडत आहे?", + "addContact": "संपर्क जोडा", + "editContact": "संपर्क संपादित करा" + }, + "button": { + "ok": "ठीक आहे", + "confirm": "खात्री करा", + "done": "पूर्ण", + "cancel": "रद्द करा", + "signIn": "साइन इन", + "signOut": "साइन आउट", + "complete": "पूर्ण करा", + "save": "जतन करा", + "generate": "निर्माण करा", + "esc": "ESC", + "keep": "ठेवा", + "tryAgain": "पुन्हा प्रयत्न करा", + "discard": "टाका", + "replace": "बदला", + "insertBelow": "खाली घाला", + "insertAbove": "वर घाला", + "upload": "अपलोड करा", + "edit": "संपादित करा", + "delete": "हटवा", + "copy": "कॉपी करा", + "duplicate": "प्रत बनवा", + "putback": "परत ठेवा", + "update": "अद्यतनित करा", + "share": "शेअर करा", + "removeFromFavorites": "आवडतीतून काढा", + "removeFromRecent": "अलीकडील यादीतून काढा", + "addToFavorites": "आवडतीत जोडा", + "favoriteSuccessfully": "आवडतीत यशस्वीरित्या जोडले", + "unfavoriteSuccessfully": "आवडतीतून यशस्वीरित्या काढले", + "duplicateSuccessfully": "प्रत यशस्वीरित्या तयार झाली", + "rename": "नाव बदला", + "helpCenter": "मदत केंद्र", + "add": "जोड़ा", + "yes": "होय", + "no": "नाही", + "clear": "साफ करा", + "remove": "काढा", + "dontRemove": "काढू नका", + "copyLink": "लिंक कॉपी करा", + "align": "जुळवा", + "login": "लॉगिन", + "logout": "लॉगआउट", + "deleteAccount": "खाते हटवा", + "back": "मागे", + "signInGoogle": "Google सह पुढे जा", + "signInGithub": "GitHub सह पुढे जा", + "signInDiscord": "Discord सह पुढे जा", + "more": "अधिक", + "create": "तयार करा", + "close": "बंद करा", + "next": "पुढे", + "previous": "मागील", + "submit": "सबमिट करा", + "download": "डाउनलोड करा", + "backToHome": "मुख्यपृष्ठावर परत जा", + "viewing": "पाहत आहात", + "editing": "संपादन करत आहात", + "gotIt": "समजले", + "retry": "पुन्हा प्रयत्न करा", + "uploadFailed": "अपलोड अयशस्वी.", + "copyLinkOriginal": "मूळ दुव्याची कॉपी करा" + }, + "label": { + "welcome": "स्वागत आहे!", + "firstName": "पहिले नाव", + "middleName": "मधले नाव", + "lastName": "आडनाव", + "stepX": "पायरी {X}" + }, + "oAuth": { + "err": { + "failedTitle": "तुमच्या खात्याशी कनेक्ट होता आले नाही.", + "failedMsg": "कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे." + }, + "google": { + "title": "GOOGLE साइन-इन", + "instruction1": "तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.", + "instruction2": "ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:", + "instruction3": "तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:", + "instruction4": "साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:" + } + }, + "settings": { + "title": "सेटिंग्ज", + "popupMenuItem": { + "settings": "सेटिंग्ज", + "members": "सदस्य", + "trash": "कचरा", + "helpAndSupport": "मदत आणि समर्थन" + }, + "sites": { + "title": "साइट्स", + "namespaceTitle": "नेमस्पेस", + "namespaceDescription": "तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा", + "namespaceHeader": "नेमस्पेस", + "homepageHeader": "मुख्यपृष्ठ", + "updateNamespace": "नेमस्पेस अद्यतनित करा", + "removeHomepage": "मुख्यपृष्ठ हटवा", + "selectHomePage": "एक पृष्ठ निवडा", + "clearHomePage": "या नेमस्पेससाठी मुख्यपृष्ठ साफ करा", + "customUrl": "स्वतःची URL", + "namespace": { + "description": "हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत", + "tooltip": "कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो", + "updateExistingNamespace": "विद्यमान नेमस्पेस अद्यतनित करा", + "upgradeToPro": "मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा", + "redirectToPayment": "पेमेंट पृष्ठावर वळवत आहे...", + "onlyWorkspaceOwnerCanSetHomePage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो", + "pleaseAskOwnerToSetHomePage": "कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा" + }, + "publishedPage": { + "title": "सर्व प्रकाशित पृष्ठे", + "description": "तुमची प्रकाशित पृष्ठे व्यवस्थापित करा", + "page": "पृष्ठ", + "pathName": "पथाचे नाव", + "date": "प्रकाशन तारीख", + "emptyHinText": "या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत", + "noPublishedPages": "प्रकाशित पृष्ठे नाहीत", + "settings": "प्रकाशन सेटिंग्ज", + "clickToOpenPageInApp": "पृष्ठ अ‍ॅपमध्ये उघडा", + "clickToOpenPageInBrowser": "पृष्ठ ब्राउझरमध्ये उघडा" + } + } + }, + "error": { + "failedToGeneratePaymentLink": "Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी", + "failedToUpdateNamespace": "नेमस्पेस अद्यतनित करण्यात अयशस्वी", + "proPlanLimitation": "नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे", + "namespaceAlreadyInUse": "नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा", + "invalidNamespace": "अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा", + "namespaceLengthAtLeast2Characters": "नेमस्पेस किमान २ अक्षरे लांब असावे", + "onlyWorkspaceOwnerCanUpdateNamespace": "फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो", + "onlyWorkspaceOwnerCanRemoveHomepage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो", + "setHomepageFailed": "मुख्यपृष्ठ सेट करण्यात अयशस्वी", + "namespaceTooLong": "नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा", + "namespaceTooShort": "नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा", + "namespaceIsReserved": "हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा", + "updatePathNameFailed": "पथाचे नाव अद्यतनित करण्यात अयशस्वी", + "removeHomePageFailed": "मुख्यपृष्ठ हटवण्यात अयशस्वी", + "publishNameContainsInvalidCharacters": "पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", + "publishNameTooShort": "पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा", + "publishNameTooLong": "पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा", + "publishNameAlreadyInUse": "हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा", + "namespaceContainsInvalidCharacters": "नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", + "publishPermissionDenied": "फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो", + "publishNameCannotBeEmpty": "पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा" + }, + "success": { + "namespaceUpdated": "नेमस्पेस यशस्वीरित्या अद्यतनित केला", + "setHomepageSuccess": "मुख्यपृष्ठ यशस्वीरित्या सेट केले", + "updatePathNameSuccess": "पथाचे नाव यशस्वीरित्या अद्यतनित केले", + "removeHomePageSuccess": "मुख्यपृष्ठ यशस्वीरित्या हटवले" + }, + "accountPage": { + "menuLabel": "खाते आणि अ‍ॅप", + "title": "माझे खाते", + "general": { + "title": "खात्याचे नाव आणि प्रोफाइल प्रतिमा", + "changeProfilePicture": "प्रोफाइल प्रतिमा बदला" + }, + "email": { + "title": "ईमेल", + "actions": { + "change": "ईमेल बदला" + } + }, + "login": { + "title": "खाते लॉगिन", + "loginLabel": "लॉगिन", + "logoutLabel": "लॉगआउट" + }, + "isUpToDate": "@:appName अद्ययावत आहे!", + "officialVersion": "आवृत्ती {version} (अधिकृत बिल्ड)" +}, + "workspacePage": { + "menuLabel": "कार्यक्षेत्र", + "title": "कार्यक्षेत्र", + "description": "तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.", + "workspaceName": { + "title": "कार्यक्षेत्राचे नाव" + }, + "workspaceIcon": { + "title": "कार्यक्षेत्राचे चिन्ह", + "description": "तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल." + }, + "appearance": { + "title": "दृश्यरूप", + "description": "कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.", + "options": { + "system": "स्वयंचलित", + "light": "लाइट", + "dark": "डार्क" + } + } + }, + "resetCursorColor": { + "title": "दस्तऐवज कर्सरचा रंग रीसेट करा", + "description": "तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?" + }, + "resetSelectionColor": { + "title": "दस्तऐवज निवडीचा रंग रीसेट करा", + "description": "तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?" + }, + "resetWidth": { + "resetSuccess": "दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली" + }, + "theme": { + "title": "थीम", + "description": "पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.", + "uploadCustomThemeTooltip": "स्वतःची थीम अपलोड करा" + }, + "workspaceFont": { + "title": "कार्यक्षेत्र फॉन्ट", + "noFontHint": "कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा." + }, + "textDirection": { + "title": "मजकूर दिशा", + "leftToRight": "डावीकडून उजवीकडे", + "rightToLeft": "उजवीकडून डावीकडे", + "auto": "स्वयंचलित", + "enableRTLItems": "RTL टूलबार घटक सक्षम करा" + }, + "layoutDirection": { + "title": "लेआउट दिशा", + "leftToRight": "डावीकडून उजवीकडे", + "rightToLeft": "उजवीकडून डावीकडे" + }, + "dateTime": { + "title": "दिनांक आणि वेळ", + "example": "{} वाजता {} ({})", + "24HourTime": "२४-तास वेळ", + "dateFormat": { + "label": "दिनांक फॉरमॅट", + "local": "स्थानिक", + "us": "US", + "iso": "ISO", + "friendly": "सुलभ", + "dmy": "D/M/Y" + } + }, + "language": { + "title": "भाषा" + }, + "deleteWorkspacePrompt": { + "title": "कार्यक्षेत्र हटवा", + "content": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील." + }, + "leaveWorkspacePrompt": { + "title": "कार्यक्षेत्र सोडा", + "content": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.", + "success": "तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.", + "fail": "कार्यक्षेत्र सोडण्यात अयशस्वी." + }, + "manageWorkspace": { + "title": "कार्यक्षेत्र व्यवस्थापित करा", + "leaveWorkspace": "कार्यक्षेत्र सोडा", + "deleteWorkspace": "कार्यक्षेत्र हटवा" + }, + "manageDataPage": { + "menuLabel": "डेटा व्यवस्थापित करा", + "title": "डेटा व्यवस्थापन", + "description": "@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.", + "dataStorage": { + "title": "फाइल संचयन स्थान", + "tooltip": "जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान", + "actions": { + "change": "मार्ग बदला", + "open": "फोल्डर उघडा", + "openTooltip": "सध्याच्या डेटा फोल्डरचे स्थान उघडा", + "copy": "मार्ग कॉपी करा", + "copiedHint": "मार्ग कॉपी केला!", + "resetTooltip": "मूलभूत स्थानावर रीसेट करा" + }, + "resetDialog": { + "title": "तुम्हाला खात्री आहे का?", + "description": "पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा." + } + }, + "importData": { + "title": "डेटा आयात करा", + "tooltip": "@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा", + "description": "बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा", + "action": "फाइल निवडा" + }, + "encryption": { + "title": "एनक्रिप्शन", + "tooltip": "तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा", + "descriptionNoEncryption": "एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.", + "descriptionEncrypted": "तुमचा डेटा एनक्रिप्टेड आहे.", + "action": "डेटा एनक्रिप्ट करा", + "dialog": { + "title": "संपूर्ण डेटा एनक्रिप्ट करायचा?", + "description": "तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?" + } + }, + "cache": { + "title": "कॅशे साफ करा", + "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", + "dialog": { + "title": "कॅशे साफ करा", + "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", + "successHint": "कॅशे साफ झाली!" + } + }, + "data": { + "fixYourData": "तुमचा डेटा सुधारा", + "fixButton": "सुधारा", + "fixYourDataDescription": "तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता." + } + }, + "shortcutsPage": { + "menuLabel": "शॉर्टकट्स", + "title": "शॉर्टकट्स", + "editBindingHint": "नवीन बाइंडिंग टाका", + "searchHint": "शोधा", + "actions": { + "resetDefault": "मूलभूत रीसेट करा" + }, + "errorPage": { + "message": "शॉर्टकट्स लोड करण्यात अयशस्वी: {}", + "howToFix": "कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा." + }, + "resetDialog": { + "title": "शॉर्टकट्स रीसेट करा", + "description": "हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?", + "buttonLabel": "रीसेट करा" + }, + "conflictDialog": { + "title": "{} आधीच वापरले जात आहे", + "descriptionPrefix": "हे कीबाइंडिंग सध्या ", + "descriptionSuffix": " यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.", + "confirmLabel": "पुढे जा" + }, + "editTooltip": "कीबाइंडिंग संपादित करण्यासाठी क्लिक करा", + "keybindings": { + "toggleToDoList": "टू-डू सूची चालू/बंद करा", + "insertNewParagraphInCodeblock": "नवीन परिच्छेद टाका", + "pasteInCodeblock": "कोडब्लॉकमध्ये पेस्ट करा", + "selectAllCodeblock": "सर्व निवडा", + "indentLineCodeblock": "ओळीच्या सुरुवातीला दोन स्पेस टाका", + "outdentLineCodeblock": "ओळीच्या सुरुवातीची दोन स्पेस काढा", + "twoSpacesCursorCodeblock": "कर्सरवर दोन स्पेस टाका", + "copy": "निवड कॉपी करा", + "paste": "मजकुरात पेस्ट करा", + "cut": "निवड कट करा", + "alignLeft": "मजकूर डावीकडे संरेखित करा", + "alignCenter": "मजकूर मधोमध संरेखित करा", + "alignRight": "मजकूर उजवीकडे संरेखित करा", + "insertInlineMathEquation": "इनलाइन गणितीय सूत्र टाका", + "undo": "पूर्ववत करा", + "redo": "पुन्हा करा", + "convertToParagraph": "ब्लॉक परिच्छेदात रूपांतरित करा", + "backspace": "हटवा", + "deleteLeftWord": "डावीकडील शब्द हटवा", + "deleteLeftSentence": "डावीकडील वाक्य हटवा", + "delete": "उजवीकडील अक्षर हटवा", + "deleteMacOS": "डावीकडील अक्षर हटवा", + "deleteRightWord": "उजवीकडील शब्द हटवा", + "moveCursorLeft": "कर्सर डावीकडे हलवा", + "moveCursorBeginning": "कर्सर सुरुवातीला हलवा", + "moveCursorLeftWord": "कर्सर एक शब्द डावीकडे हलवा", + "moveCursorLeftSelect": "निवडा आणि कर्सर डावीकडे हलवा", + "moveCursorBeginSelect": "निवडा आणि कर्सर सुरुवातीला हलवा", + "moveCursorLeftWordSelect": "निवडा आणि कर्सर एक शब्द डावीकडे हलवा", + "moveCursorRight": "कर्सर उजवीकडे हलवा", + "moveCursorEnd": "कर्सर शेवटी हलवा", + "moveCursorRightWord": "कर्सर एक शब्द उजवीकडे हलवा", + "moveCursorRightSelect": "निवडा आणि कर्सर उजवीकडे हलवा", + "moveCursorEndSelect": "निवडा आणि कर्सर शेवटी हलवा", + "moveCursorRightWordSelect": "निवडा आणि कर्सर एक शब्द उजवीकडे हलवा", + "moveCursorUp": "कर्सर वर हलवा", + "moveCursorTopSelect": "निवडा आणि कर्सर वर हलवा", + "moveCursorTop": "कर्सर वर हलवा", + "moveCursorUpSelect": "निवडा आणि कर्सर वर हलवा", + "moveCursorBottomSelect": "निवडा आणि कर्सर खाली हलवा", + "moveCursorBottom": "कर्सर खाली हलवा", + "moveCursorDown": "कर्सर खाली हलवा", + "moveCursorDownSelect": "निवडा आणि कर्सर खाली हलवा", + "home": "वर स्क्रोल करा", + "end": "खाली स्क्रोल करा", + "toggleBold": "बोल्ड चालू/बंद करा", + "toggleItalic": "इटालिक चालू/बंद करा", + "toggleUnderline": "अधोरेखित चालू/बंद करा", + "toggleStrikethrough": "स्ट्राईकथ्रू चालू/बंद करा", + "toggleCode": "इनलाइन कोड चालू/बंद करा", + "toggleHighlight": "हायलाईट चालू/बंद करा", + "showLinkMenu": "लिंक मेनू दाखवा", + "openInlineLink": "इनलाइन लिंक उघडा", + "openLinks": "सर्व निवडलेले लिंक उघडा", + "indent": "इंडेंट", + "outdent": "आउटडेंट", + "exit": "संपादनातून बाहेर पडा", + "pageUp": "एक पृष्ठ वर स्क्रोल करा", + "pageDown": "एक पृष्ठ खाली स्क्रोल करा", + "selectAll": "सर्व निवडा", + "pasteWithoutFormatting": "फॉरमॅटिंगशिवाय पेस्ट करा", + "showEmojiPicker": "इमोजी निवडकर्ता दाखवा", + "enterInTableCell": "टेबलमध्ये नवीन ओळ जोडा", + "leftInTableCell": "टेबलमध्ये डावीकडील सेलमध्ये जा", + "rightInTableCell": "टेबलमध्ये उजवीकडील सेलमध्ये जा", + "upInTableCell": "टेबलमध्ये वरील सेलमध्ये जा", + "downInTableCell": "टेबलमध्ये खालील सेलमध्ये जा", + "tabInTableCell": "टेबलमध्ये पुढील सेलमध्ये जा", + "shiftTabInTableCell": "टेबलमध्ये मागील सेलमध्ये जा", + "backSpaceInTableCell": "सेलच्या सुरुवातीला थांबा" + }, + "commands": { + "codeBlockNewParagraph": "कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका", + "codeBlockIndentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका", + "codeBlockOutdentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा", + "codeBlockAddTwoSpaces": "कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका", + "codeBlockSelectAll": "कोडब्लॉकमधील सर्व मजकूर निवडा", + "codeBlockPasteText": "कोडब्लॉकमध्ये मजकूर पेस्ट करा", + "textAlignLeft": "मजकूर डावीकडे संरेखित करा", + "textAlignCenter": "मजकूर मधोमध संरेखित करा", + "textAlignRight": "मजकूर उजवीकडे संरेखित करा" + }, + "couldNotLoadErrorMsg": "शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा", + "couldNotSaveErrorMsg": "शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा" +}, + "aiPage": { + "title": "AI सेटिंग्ज", + "menuLabel": "AI सेटिंग्ज", + "keys": { + "enableAISearchTitle": "AI शोध", + "aiSettingsDescription": "AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.", + "loginToEnableAIFeature": "AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.", + "llmModel": "भाषा मॉडेल", + "llmModelType": "भाषा मॉडेल प्रकार", + "downloadLLMPrompt": "{} डाउनलोड करा", + "downloadAppFlowyOfflineAI": "AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?", + "downloadLLMPromptDetail": "{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?", + "downloadBigFilePrompt": "डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात", + "downloadAIModelButton": "डाउनलोड करा", + "downloadingModel": "डाउनलोड करत आहे", + "localAILoaded": "स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे", + "localAIStart": "स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा", + "localAILoading": "स्थानिक AI Chat मॉडेल लोड होत आहे...", + "localAIStopped": "स्थानिक AI थांबले आहे", + "localAIRunning": "स्थानिक AI चालू आहे", + "localAINotReadyRetryLater": "स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा", + "localAIDisabled": "तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा", + "localAIInitializing": "स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात", + "localAINotReadyTextFieldPrompt": "स्थानिक AI लोड होत असताना संपादन करता येणार नाही", + "failToLoadLocalAI": "स्थानिक AI सुरू करण्यात अयशस्वी.", + "restartLocalAI": "पुन्हा सुरू करा", + "disableLocalAITitle": "स्थानिक AI अकार्यक्षम करा", + "disableLocalAIDescription": "तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?", + "localAIToggleTitle": "AppFlowy स्थानिक AI (LAI)", + "localAIToggleSubTitle": "AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा", + "offlineAIInstruction1": "हे अनुसरा", + "offlineAIInstruction2": "सूचना", + "offlineAIInstruction3": "ऑफलाइन AI सक्षम करण्यासाठी.", + "offlineAIDownload1": "जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया", + "offlineAIDownload2": "डाउनलोड", + "offlineAIDownload3": "करा", + "activeOfflineAI": "सक्रिय", + "downloadOfflineAI": "डाउनलोड करा", + "openModelDirectory": "फोल्डर उघडा", + "laiNotReady": "स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.", + "ollamaNotReady": "Ollama सर्व्हर तयार नाही.", + "pleaseFollowThese": "कृपया हे अनुसरा", + "instructions": "सूचना", + "installOllamaLai": "Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.", + "modelsMissing": "आवश्यक मॉडेल्स सापडत नाहीत.", + "downloadModel": "त्यांना डाउनलोड करण्यासाठी." + } +}, + "planPage": { + "menuLabel": "योजना", + "title": "दर योजना", + "planUsage": { + "title": "योजनेचा वापर सारांश", + "storageLabel": "स्टोरेज", + "storageUsage": "{} पैकी {} GB", + "unlimitedStorageLabel": "अमर्यादित स्टोरेज", + "collaboratorsLabel": "सदस्य", + "collaboratorsUsage": "{} पैकी {}", + "aiResponseLabel": "AI प्रतिसाद", + "aiResponseUsage": "{} पैकी {}", + "unlimitedAILabel": "अमर्यादित AI प्रतिसाद", + "proBadge": "प्रो", + "aiMaxBadge": "AI Max", + "aiOnDeviceBadge": "मॅकसाठी ऑन-डिव्हाइस AI", + "memberProToggle": "अधिक सदस्य आणि अमर्यादित AI", + "aiMaxToggle": "अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश", + "aiOnDeviceToggle": "जास्त गोपनीयतेसाठी स्थानिक AI", + "aiCredit": { + "title": "@:appName AI क्रेडिट जोडा", + "price": "{}", + "priceDescription": "1,000 क्रेडिट्ससाठी", + "purchase": "AI खरेदी करा", + "info": "प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:", + "infoItemOne": "प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद", + "infoItemTwo": "प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद" + }, + "currentPlan": { + "bannerLabel": "सद्य योजना", + "freeTitle": "फ्री", + "proTitle": "प्रो", + "teamTitle": "टीम", + "freeInfo": "2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम", + "proInfo": "10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य", + "teamInfo": "सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य", + "upgrade": "योजना बदला", + "canceledInfo": "तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल." + }, + "addons": { + "title": "ऍड-ऑन्स", + "addLabel": "जोडा", + "activeLabel": "जोडले गेले", + "aiMax": { + "title": "AI Max", + "description": "प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा", + "price": "{}", + "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)" + }, + "aiOnDevice": { + "title": "मॅकसाठी ऑन-डिव्हाइस AI", + "description": "तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा", + "price": "{}", + "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)", + "recommend": "M1 किंवा नवीनतम शिफारस केली जाते" + } + }, + "deal": { + "bannerLabel": "नववर्षाचे विशेष ऑफर!", + "title": "तुमची टीम वाढवा!", + "info": "Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.", + "viewPlans": "योजना पहा" + } + } +}, + "billingPage": { + "menuLabel": "बिलिंग", + "title": "बिलिंग", + "plan": { + "title": "योजना", + "freeLabel": "फ्री", + "proLabel": "प्रो", + "planButtonLabel": "योजना बदला", + "billingPeriod": "बिलिंग कालावधी", + "periodButtonLabel": "कालावधी संपादित करा" + }, + "paymentDetails": { + "title": "पेमेंट तपशील", + "methodLabel": "पेमेंट पद्धत", + "methodButtonLabel": "पद्धत संपादित करा" + }, + "addons": { + "title": "ऍड-ऑन्स", + "addLabel": "जोडा", + "removeLabel": "काढा", + "renewLabel": "नवीन करा", + "aiMax": { + "label": "AI Max", + "description": "अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा", + "activeDescription": "पुढील बिलिंग तारीख {} आहे", + "canceledDescription": "AI Max {} पर्यंत उपलब्ध असेल" + }, + "aiOnDevice": { + "label": "मॅकसाठी ऑन-डिव्हाइस AI", + "description": "तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा", + "activeDescription": "पुढील बिलिंग तारीख {} आहे", + "canceledDescription": "मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल" + }, + "removeDialog": { + "title": "{} काढा", + "description": "तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल." + } + }, + "currentPeriodBadge": "सद्य कालावधी", + "changePeriod": "कालावधी बदला", + "planPeriod": "{} कालावधी", + "monthlyInterval": "मासिक", + "monthlyPriceInfo": "प्रति सदस्य, मासिक बिलिंग", + "annualInterval": "वार्षिक", + "annualPriceInfo": "प्रति सदस्य, वार्षिक बिलिंग" +}, + "comparePlanDialog": { + "title": "योजना तुलना आणि निवड", + "planFeatures": "योजनेची\nवैशिष्ट्ये", + "current": "सध्याची", + "actions": { + "upgrade": "अपग्रेड करा", + "downgrade": "डाऊनग्रेड करा", + "current": "सध्याची" + }, + "freePlan": { + "title": "फ्री", + "description": "२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी", + "price": "{}", + "priceInfo": "सदैव फ्री" + }, + "proPlan": { + "title": "प्रो", + "description": "लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी", + "price": "{}", + "priceInfo": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग\n\n{} मासिक बिलिंगसाठी" + }, + "planLabels": { + "itemOne": "वर्कस्पेसेस", + "itemTwo": "सदस्य", + "itemThree": "स्टोरेज", + "itemFour": "रिअल-टाइम सहकार्य", + "itemFive": "मोबाईल अ‍ॅप", + "itemSix": "AI प्रतिसाद", + "itemSeven": "AI प्रतिमा", + "itemFileUpload": "फाइल अपलोड", + "customNamespace": "सानुकूल नेमस्पेस", + "tooltipSix": "‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही", + "intelligentSearch": "स्मार्ट शोध", + "tooltipSeven": "तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते", + "customNamespaceTooltip": "सानुकूल प्रकाशित साइट URL" + }, + "freeLabels": { + "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", + "itemTwo": "२ पर्यंत", + "itemThree": "५ GB", + "itemFour": "होय", + "itemFive": "होय", + "itemSix": "१० कायमस्वरूपी", + "itemSeven": "२ कायमस्वरूपी", + "itemFileUpload": "७ MB पर्यंत", + "intelligentSearch": "स्मार्ट शोध" + }, + "proLabels": { + "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", + "itemTwo": "१० पर्यंत", + "itemThree": "अमर्यादित", + "itemFour": "होय", + "itemFive": "होय", + "itemSix": "अमर्यादित", + "itemSeven": "दर महिन्याला १० प्रतिमा", + "itemFileUpload": "अमर्यादित", + "intelligentSearch": "स्मार्ट शोध" + }, + "paymentSuccess": { + "title": "तुम्ही आता {} योजनेवर आहात!", + "description": "तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता." + }, + "downgradeDialog": { + "title": "तुम्हाला योजना डाऊनग्रेड करायची आहे का?", + "description": "तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.", + "downgradeLabel": "योजना डाऊनग्रेड करा" + } +}, + "cancelSurveyDialog": { + "title": "तुम्ही जात आहात याचे दुःख आहे", + "description": "तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.", + "commonOther": "इतर", + "otherHint": "तुमचे उत्तर येथे लिहा", + "questionOne": { + "question": "तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?", + "answerOne": "खर्च खूप जास्त आहे", + "answerTwo": "वैशिष्ट्ये अपेक्षांनुसार नव्हती", + "answerThree": "यापेक्षा चांगला पर्याय सापडला", + "answerFour": "वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही", + "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" + }, + "questionTwo": { + "question": "भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?", + "answerOne": "खूप शक्यता आहे", + "answerTwo": "काहीशी शक्यता आहे", + "answerThree": "निश्चित नाही", + "answerFour": "अल्प शक्यता", + "answerFive": "एकदम कमी शक्यता" + }, + "questionThree": { + "question": "तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?", + "answerOne": "अनेक वापरकर्त्यांशी सहकार्य", + "answerTwo": "लांब कालावधीची आवृत्ती इतिहास", + "answerThree": "अमर्यादित AI प्रतिसाद", + "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" + }, + "questionFour": { + "question": "@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?", + "answerOne": "खूप छान", + "answerTwo": "चांगला", + "answerThree": "सरासरी", + "answerFour": "सरासरीपेक्षा कमी", + "answerFive": "असंतोषजनक" + } +}, + "common": { + "uploadingFile": "फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका", + "uploadNotionSuccess": "तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल", + "reset": "रीसेट करा" +}, + "menu": { + "appearance": "दृश्यरूप", + "language": "भाषा", + "user": "वापरकर्ता", + "files": "फाईल्स", + "notifications": "सूचना", + "open": "सेटिंग्ज उघडा", + "logout": "लॉगआउट", + "logoutPrompt": "तुम्हाला नक्की लॉगआउट करायचे आहे का?", + "selfEncryptionLogoutPrompt": "तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे", + "syncSetting": "सिंक्रोनायझेशन सेटिंग", + "cloudSettings": "क्लाऊड सेटिंग्ज", + "enableSync": "सिंक्रोनायझेशन सक्षम करा", + "enableSyncLog": "सिंक लॉगिंग सक्षम करा", + "enableSyncLogWarning": "सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा", + "enableEncrypt": "डेटा एन्क्रिप्ट करा", + "cloudURL": "बेस URL", + "webURL": "वेब URL", + "invalidCloudURLScheme": "अवैध स्कीम", + "cloudServerType": "क्लाऊड सर्व्हर", + "cloudServerTypeTip": "कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते", + "cloudLocal": "स्थानिक", + "cloudAppFlowy": "@:appName Cloud", + "cloudAppFlowySelfHost": "@:appName क्लाऊड सेल्फ-होस्टेड", + "appFlowyCloudUrlCanNotBeEmpty": "क्लाऊड URL रिकामा असू शकत नाही", + "clickToCopy": "क्लिपबोर्डवर कॉपी करा", + "selfHostStart": "जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा", + "selfHostContent": "दस्तऐवज", + "selfHostEnd": "तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी", + "pleaseInputValidURL": "कृपया वैध URL टाका", + "changeUrl": "सेल्फ-होस्टेड URL {} मध्ये बदला", + "cloudURLHint": "तुमच्या सर्व्हरचा बेस URL टाका", + "webURLHint": "तुमच्या वेब सर्व्हरचा बेस URL टाका", + "cloudWSURL": "वेबसॉकेट URL", + "cloudWSURLHint": "तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका", + "restartApp": "अ‍ॅप रीस्टार्ट करा", + "restartAppTip": "बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.", + "changeServerTip": "सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे", + "enableEncryptPrompt": "तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा", + "inputEncryptPrompt": "कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:", + "clickToCopySecret": "गुप्तकी कॉपी करण्यासाठी क्लिक करा", + "configServerSetting": "तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा", + "configServerGuide": "`Quick Start` निवडल्यानंतर, `Settings` → \"Cloud Settings\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.", + "inputTextFieldHint": "तुमची गुप्तकी", + "historicalUserList": "वापरकर्ता लॉगिन इतिहास", + "historicalUserListTooltip": "ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात", + "openHistoricalUser": "अनामिक खाते उघडण्यासाठी क्लिक करा", + "customPathPrompt": "@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.", + "importAppFlowyData": "बाह्य @:appName फोल्डरमधून डेटा आयात करा", + "importingAppFlowyDataTip": "डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका", + "importAppFlowyDataDescription": "बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा", + "importSuccess": "@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला", + "importFailed": "@:appName डेटा फोल्डर आयात करण्यात अयशस्वी", + "importGuide": "अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा" +}, + "notifications": { + "enableNotifications": { + "label": "सूचना सक्षम करा", + "hint": "स्थानिक सूचना दिसू नयेत यासाठी बंद करा." + }, + "showNotificationsIcon": { + "label": "सूचना चिन्ह दाखवा", + "hint": "साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा." + }, + "archiveNotifications": { + "allSuccess": "सर्व सूचना यशस्वीरित्या संग्रहित केल्या", + "success": "सूचना यशस्वीरित्या संग्रहित केली" + }, + "markAsReadNotifications": { + "allSuccess": "सर्व वाचलेल्या म्हणून चिन्हांकित केल्या", + "success": "वाचलेले म्हणून चिन्हांकित केले" + }, + "action": { + "markAsRead": "वाचलेले म्हणून चिन्हांकित करा", + "multipleChoice": "अधिक निवडा", + "archive": "संग्रहित करा" + }, + "settings": { + "settings": "सेटिंग्ज", + "markAllAsRead": "सर्व वाचलेले म्हणून चिन्हांकित करा", + "archiveAll": "सर्व संग्रहित करा" + }, + "emptyInbox": { + "title": "इनबॉक्स झिरो!", + "description": "इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा." + }, + "emptyUnread": { + "title": "कोणतीही न वाचलेली सूचना नाही", + "description": "तुम्ही सर्व वाचले आहे!" + }, + "emptyArchived": { + "title": "कोणतीही संग्रहित सूचना नाही", + "description": "संग्रहित सूचना इथे दिसतील." + }, + "tabs": { + "inbox": "इनबॉक्स", + "unread": "न वाचलेले", + "archived": "संग्रहित" + }, + "refreshSuccess": "सूचना यशस्वीरित्या रीफ्रेश केल्या", + "titles": { + "notifications": "सूचना", + "reminder": "रिमाइंडर" + } +}, + "appearance": { + "resetSetting": "रीसेट", + "fontFamily": { + "label": "फॉन्ट फॅमिली", + "search": "शोध", + "defaultFont": "सिस्टम" + }, + "themeMode": { + "label": "थीम मोड", + "light": "लाइट मोड", + "dark": "डार्क मोड", + "system": "सिस्टमशी जुळवा" + }, + "fontScaleFactor": "फॉन्ट स्केल घटक", + "displaySize": "डिस्प्ले आकार", + "documentSettings": { + "cursorColor": "डॉक्युमेंट कर्सरचा रंग", + "selectionColor": "डॉक्युमेंट निवडीचा रंग", + "width": "डॉक्युमेंटची रुंदी", + "changeWidth": "बदला", + "pickColor": "रंग निवडा", + "colorShade": "रंगाची छटा", + "opacity": "अपारदर्शकता", + "hexEmptyError": "Hex रंग रिकामा असू शकत नाही", + "hexLengthError": "Hex व्हॅल्यू 6 अंकांची असावी", + "hexInvalidError": "अवैध Hex व्हॅल्यू", + "opacityEmptyError": "अपारदर्शकता रिकामी असू शकत नाही", + "opacityRangeError": "अपारदर्शकता 1 ते 100 दरम्यान असावी", + "app": "अ‍ॅप", + "flowy": "Flowy", + "apply": "लागू करा" + }, + "layoutDirection": { + "label": "लेआउट दिशा", + "hint": "तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.", + "ltr": "LTR", + "rtl": "RTL" + }, + "textDirection": { + "label": "मूलभूत मजकूर दिशा", + "hint": "मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "स्वयं", + "fallback": "लेआउट दिशेशी जुळवा" + }, + "themeUpload": { + "button": "अपलोड", + "uploadTheme": "थीम अपलोड करा", + "description": "खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.", + "loading": "कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...", + "uploadSuccess": "तुमची थीम यशस्वीरित्या अपलोड झाली आहे", + "deletionFailure": "थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.", + "filePickerDialogTitle": ".flowy_plugin फाईल निवडा", + "urlUploadFailure": "URL उघडण्यात अयशस्वी: {}" + }, + "theme": "थीम", + "builtInsLabel": "अंतर्गत थीम्स", + "pluginsLabel": "प्लगइन्स", + "dateFormat": { + "label": "दिनांक फॉरमॅट", + "local": "स्थानिक", + "us": "US", + "iso": "ISO", + "friendly": "अनौपचारिक", + "dmy": "D/M/Y" + }, + "timeFormat": { + "label": "वेळ फॉरमॅट", + "twelveHour": "१२ तास", + "twentyFourHour": "२४ तास" + }, + "showNamingDialogWhenCreatingPage": "पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा", + "enableRTLToolbarItems": "RTL टूलबार आयटम्स सक्षम करा", + "members": { + "title": "सदस्य सेटिंग्ज", + "inviteMembers": "सदस्यांना आमंत्रण द्या", + "inviteHint": "ईमेलद्वारे आमंत्रण द्या", + "sendInvite": "आमंत्रण पाठवा", + "copyInviteLink": "आमंत्रण दुवा कॉपी करा", + "label": "सदस्य", + "user": "वापरकर्ता", + "role": "भूमिका", + "removeFromWorkspace": "वर्कस्पेसमधून काढा", + "removeFromWorkspaceSuccess": "वर्कस्पेसमधून यशस्वीरित्या काढले", + "removeFromWorkspaceFailed": "वर्कस्पेसमधून काढण्यात अयशस्वी", + "owner": "मालक", + "guest": "अतिथी", + "member": "सदस्य", + "memberHintText": "सदस्य पृष्ठे वाचू व संपादित करू शकतो", + "guestHintText": "अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.", + "emailInvalidError": "अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा", + "emailSent": "ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा", + "members": "सदस्य", + "membersCount": { + "zero": "{} सदस्य", + "one": "{} सदस्य", + "other": "{} सदस्य" + }, + "inviteFailedDialogTitle": "आमंत्रण पाठवण्यात अयशस्वी", + "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.", + "inviteFailedMemberLimitMobile": "तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.", + "memberLimitExceeded": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया ", + "memberLimitExceededUpgrade": "अपग्रेड करा", + "memberLimitExceededPro": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा", + "memberLimitExceededProContact": "support@appflowy.io", + "failedToAddMember": "सदस्य जोडण्यात अयशस्वी", + "addMemberSuccess": "सदस्य यशस्वीरित्या जोडला गेला", + "removeMember": "सदस्य काढा", + "areYouSureToRemoveMember": "तुम्हाला हा सदस्य काढायचा आहे का?", + "inviteMemberSuccess": "आमंत्रण यशस्वीरित्या पाठवले गेले", + "failedToInviteMember": "सदस्य आमंत्रित करण्यात अयशस्वी", + "workspaceMembersError": "अरे! काहीतरी चूक झाली आहे", + "workspaceMembersErrorDescription": "आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा" + } +}, + "files": { + "copy": "कॉपी करा", + "defaultLocation": "फाईल्स आणि डेटाचा संचय स्थान", + "exportData": "तुमचा डेटा निर्यात करा", + "doubleTapToCopy": "पथ कॉपी करण्यासाठी दोनदा टॅप करा", + "restoreLocation": "@:appName चे मूळ स्थान पुनर्संचयित करा", + "customizeLocation": "इतर फोल्डर उघडा", + "restartApp": "बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.", + "exportDatabase": "डेटाबेस निर्यात करा", + "selectFiles": "निर्यात करण्यासाठी फाईल्स निवडा", + "selectAll": "सर्व निवडा", + "deselectAll": "सर्व निवड रद्द करा", + "createNewFolder": "नवीन फोल्डर तयार करा", + "createNewFolderDesc": "तुमचा डेटा कुठे साठवायचा हे सांगा", + "defineWhereYourDataIsStored": "तुमचा डेटा कुठे साठवला जातो हे ठरवा", + "open": "उघडा", + "openFolder": "आधीक फोल्डर उघडा", + "openFolderDesc": "तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा", + "folderHintText": "फोल्डरचे नाव", + "location": "नवीन फोल्डर तयार करत आहे", + "locationDesc": "तुमच्या @:appName डेटासाठी नाव निवडा", + "browser": "ब्राउझ करा", + "create": "तयार करा", + "set": "सेट करा", + "folderPath": "फोल्डर साठवण्याचा मार्ग", + "locationCannotBeEmpty": "मार्ग रिकामा असू शकत नाही", + "pathCopiedSnackbar": "फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!", + "changeLocationTooltips": "डेटा डिरेक्टरी बदला", + "change": "बदला", + "openLocationTooltips": "इतर डेटा डिरेक्टरी उघडा", + "openCurrentDataFolder": "सध्याची डेटा डिरेक्टरी उघडा", + "recoverLocationTooltips": "@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा", + "exportFileSuccess": "फाईल यशस्वीरित्या निर्यात झाली!", + "exportFileFail": "फाईल निर्यात करण्यात अयशस्वी!", + "export": "निर्यात करा", + "clearCache": "कॅशे साफ करा", + "clearCacheDesc": "प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.", + "areYouSureToClearCache": "तुम्हाला नक्की कॅशे साफ करायची आहे का?", + "clearCacheSuccess": "कॅशे यशस्वीरित्या साफ झाली!" +}, + "user": { + "name": "नाव", + "email": "ईमेल", + "tooltipSelectIcon": "चिन्ह निवडा", + "selectAnIcon": "चिन्ह निवडा", + "pleaseInputYourOpenAIKey": "कृपया तुमची AI की टाका", + "clickToLogout": "सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा" +}, + "mobile": { + "personalInfo": "वैयक्तिक माहिती", + "username": "वापरकर्तानाव", + "usernameEmptyError": "वापरकर्तानाव रिकामे असू शकत नाही", + "about": "विषयी", + "pushNotifications": "पुश सूचना", + "support": "सपोर्ट", + "joinDiscord": "Discord मध्ये सहभागी व्हा", + "privacyPolicy": "गोपनीयता धोरण", + "userAgreement": "वापरकर्ता करार", + "termsAndConditions": "अटी व शर्ती", + "userprofileError": "वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी", + "userprofileErrorDescription": "कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.", + "selectLayout": "लेआउट निवडा", + "selectStartingDay": "सप्ताहाचा प्रारंभ दिवस निवडा", + "version": "आवृत्ती" +}, + "grid": { + "deleteView": "तुम्हाला हे दृश्य हटवायचे आहे का?", + "createView": "नवीन", + "title": { + "placeholder": "नाव नाही" + }, + "settings": { + "filter": "फिल्टर", + "sort": "क्रमवारी", + "sortBy": "यावरून क्रमवारी लावा", + "properties": "गुणधर्म", + "reorderPropertiesTooltip": "गुणधर्मांचे स्थान बदला", + "group": "समूह", + "addFilter": "फिल्टर जोडा", + "deleteFilter": "फिल्टर हटवा", + "filterBy": "यावरून फिल्टर करा", + "typeAValue": "मूल्य लिहा...", + "layout": "लेआउट", + "compactMode": "कॉम्पॅक्ट मोड", + "databaseLayout": "लेआउट", + "viewList": { + "zero": "० दृश्ये", + "one": "{count} दृश्य", + "other": "{count} दृश्ये" + }, + "editView": "दृश्य संपादित करा", + "boardSettings": "बोर्ड सेटिंग", + "calendarSettings": "कॅलेंडर सेटिंग", + "createView": "नवीन दृश्य", + "duplicateView": "दृश्याची प्रत बनवा", + "deleteView": "दृश्य हटवा", + "numberOfVisibleFields": "{} दर्शविले" + }, + "filter": { + "empty": "कोणतेही सक्रिय फिल्टर नाहीत", + "addFilter": "फिल्टर जोडा", + "cannotFindCreatableField": "फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही", + "conditon": "अट", + "where": "जिथे" + }, + "textFilter": { + "contains": "अंतर्भूत आहे", + "doesNotContain": "अंतर्भूत नाही", + "endsWith": "याने समाप्त होते", + "startWith": "याने सुरू होते", + "is": "आहे", + "isNot": "नाही", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही", + "choicechipPrefix": { + "isNot": "नाही", + "startWith": "याने सुरू होते", + "endWith": "याने समाप्त होते", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" + } + }, + "checkboxFilter": { + "isChecked": "निवडलेले आहे", + "isUnchecked": "निवडलेले नाही", + "choicechipPrefix": { + "is": "आहे" + } + }, + "checklistFilter": { + "isComplete": "पूर्ण झाले आहे", + "isIncomplted": "अपूर्ण आहे" + }, + "selectOptionFilter": { + "is": "आहे", + "isNot": "नाही", + "contains": "अंतर्भूत आहे", + "doesNotContain": "अंतर्भूत नाही", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" +}, +"dateFilter": { + "is": "या दिवशी आहे", + "before": "पूर्वी आहे", + "after": "नंतर आहे", + "onOrBefore": "या दिवशी किंवा त्याआधी आहे", + "onOrAfter": "या दिवशी किंवा त्यानंतर आहे", + "between": "दरम्यान आहे", + "empty": "रिकामे आहे", + "notEmpty": "रिकामे नाही", + "startDate": "सुरुवातीची तारीख", + "endDate": "शेवटची तारीख", + "choicechipPrefix": { + "before": "पूर्वी", + "after": "नंतर", + "between": "दरम्यान", + "onOrBefore": "या दिवशी किंवा त्याआधी", + "onOrAfter": "या दिवशी किंवा त्यानंतर", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" + } +}, +"numberFilter": { + "equal": "बरोबर आहे", + "notEqual": "बरोबर नाही", + "lessThan": "पेक्षा कमी आहे", + "greaterThan": "पेक्षा जास्त आहे", + "lessThanOrEqualTo": "किंवा कमी आहे", + "greaterThanOrEqualTo": "किंवा जास्त आहे", + "isEmpty": "रिकामे आहे", + "isNotEmpty": "रिकामे नाही" +}, +"field": { + "label": "गुणधर्म", + "hide": "गुणधर्म लपवा", + "show": "गुणधर्म दर्शवा", + "insertLeft": "डावीकडे जोडा", + "insertRight": "उजवीकडे जोडा", + "duplicate": "प्रत बनवा", + "delete": "हटवा", + "wrapCellContent": "पाठ लपेटा", + "clear": "सेल्स रिकामे करा", + "switchPrimaryFieldTooltip": "प्राथमिक फील्डचा प्रकार बदलू शकत नाही", + "textFieldName": "मजकूर", + "checkboxFieldName": "चेकबॉक्स", + "dateFieldName": "तारीख", + "updatedAtFieldName": "शेवटचे अपडेट", + "createdAtFieldName": "तयार झाले", + "numberFieldName": "संख्या", + "singleSelectFieldName": "सिंगल सिलेक्ट", + "multiSelectFieldName": "मल्टीसिलेक्ट", + "urlFieldName": "URL", + "checklistFieldName": "चेकलिस्ट", + "relationFieldName": "संबंध", + "summaryFieldName": "AI सारांश", + "timeFieldName": "वेळ", + "mediaFieldName": "फाईल्स आणि मीडिया", + "translateFieldName": "AI भाषांतर", + "translateTo": "मध्ये भाषांतर करा", + "numberFormat": "संख्या स्वरूप", + "dateFormat": "तारीख स्वरूप", + "includeTime": "वेळ जोडा", + "isRange": "शेवटची तारीख", + "dateFormatFriendly": "महिना दिवस, वर्ष", + "dateFormatISO": "वर्ष-महिना-दिनांक", + "dateFormatLocal": "महिना/दिवस/वर्ष", + "dateFormatUS": "वर्ष/महिना/दिवस", + "dateFormatDayMonthYear": "दिवस/महिना/वर्ष", + "timeFormat": "वेळ स्वरूप", + "invalidTimeFormat": "अवैध स्वरूप", + "timeFormatTwelveHour": "१२ तास", + "timeFormatTwentyFourHour": "२४ तास", + "clearDate": "तारीख हटवा", + "dateTime": "तारीख व वेळ", + "startDateTime": "सुरुवातीची तारीख व वेळ", + "endDateTime": "शेवटची तारीख व वेळ", + "failedToLoadDate": "तारीख मूल्य लोड करण्यात अयशस्वी", + "selectTime": "वेळ निवडा", + "selectDate": "तारीख निवडा", + "visibility": "दृश्यता", + "propertyType": "गुणधर्माचा प्रकार", + "addSelectOption": "पर्याय जोडा", + "typeANewOption": "नवीन पर्याय लिहा", + "optionTitle": "पर्याय", + "addOption": "पर्याय जोडा", + "editProperty": "गुणधर्म संपादित करा", + "newProperty": "नवीन गुणधर्म", + "openRowDocument": "पृष्ठ म्हणून उघडा", + "deleteFieldPromptMessage": "तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल", + "clearFieldPromptMessage": "तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील", + "newColumn": "नवीन कॉलम", + "format": "स्वरूप", + "reminderOnDateTooltip": "या सेलमध्ये अनुस्मारक शेड्यूल केले आहे", + "optionAlreadyExist": "पर्याय आधीच अस्तित्वात आहे" +}, + "rowPage": { + "newField": "नवीन फील्ड जोडा", + "fieldDragElementTooltip": "मेनू उघडण्यासाठी क्लिक करा", + "showHiddenFields": { + "one": "{count} लपलेले फील्ड दाखवा", + "many": "{count} लपलेली फील्ड दाखवा", + "other": "{count} लपलेली फील्ड दाखवा" + }, + "hideHiddenFields": { + "one": "{count} लपलेले फील्ड लपवा", + "many": "{count} लपलेली फील्ड लपवा", + "other": "{count} लपलेली फील्ड लपवा" + }, + "openAsFullPage": "पूर्ण पृष्ठ म्हणून उघडा", + "moreRowActions": "अधिक पंक्ती क्रिया" +}, +"sort": { + "ascending": "चढत्या क्रमाने", + "descending": "उतरत्या क्रमाने", + "by": "द्वारे", + "empty": "सक्रिय सॉर्ट्स नाहीत", + "cannotFindCreatableField": "सॉर्टसाठी योग्य फील्ड सापडले नाही", + "deleteAllSorts": "सर्व सॉर्ट्स हटवा", + "addSort": "सॉर्ट जोडा", + "sortsActive": "सॉर्टिंग करत असताना {intention} करू शकत नाही", + "removeSorting": "या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?", + "fieldInUse": "या फील्डवर आधीच सॉर्टिंग चालू आहे" +}, +"row": { + "label": "पंक्ती", + "duplicate": "प्रत बनवा", + "delete": "हटवा", + "titlePlaceholder": "शीर्षक नाही", + "textPlaceholder": "रिक्त", + "copyProperty": "गुणधर्म क्लिपबोर्डवर कॉपी केला", + "count": "संख्या", + "newRow": "नवीन पंक्ती", + "loadMore": "अधिक लोड करा", + "action": "क्रिया", + "add": "खाली जोडा वर क्लिक करा", + "drag": "हलवण्यासाठी ड्रॅग करा", + "deleteRowPrompt": "तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "deleteCardPrompt": "तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "dragAndClick": "हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा", + "insertRecordAbove": "वर रेकॉर्ड जोडा", + "insertRecordBelow": "खाली रेकॉर्ड जोडा", + "noContent": "माहिती नाही", + "reorderRowDescription": "पंक्तीचे पुन्हा क्रमांकन", + "createRowAboveDescription": "वर पंक्ती तयार करा", + "createRowBelowDescription": "खाली पंक्ती जोडा" +}, +"selectOption": { + "create": "तयार करा", + "purpleColor": "जांभळा", + "pinkColor": "गुलाबी", + "lightPinkColor": "फिकट गुलाबी", + "orangeColor": "नारंगी", + "yellowColor": "पिवळा", + "limeColor": "लिंबू", + "greenColor": "हिरवा", + "aquaColor": "आक्वा", + "blueColor": "निळा", + "deleteTag": "टॅग हटवा", + "colorPanelTitle": "रंग", + "panelTitle": "पर्याय निवडा किंवा नवीन तयार करा", + "searchOption": "पर्याय शोधा", + "searchOrCreateOption": "पर्याय शोधा किंवा तयार करा", + "createNew": "नवीन तयार करा", + "orSelectOne": "किंवा पर्याय निवडा", + "typeANewOption": "नवीन पर्याय टाइप करा", + "tagName": "टॅग नाव" +}, +"checklist": { + "taskHint": "कार्याचे वर्णन", + "addNew": "नवीन कार्य जोडा", + "submitNewTask": "तयार करा", + "hideComplete": "पूर्ण कार्ये लपवा", + "showComplete": "सर्व कार्ये दाखवा" +}, +"url": { + "launch": "बाह्य ब्राउझरमध्ये लिंक उघडा", + "copy": "लिंक क्लिपबोर्डवर कॉपी करा", + "textFieldHint": "URL टाका", + "copiedNotification": "क्लिपबोर्डवर कॉपी केले!" +}, +"relation": { + "relatedDatabasePlaceLabel": "संबंधित डेटाबेस", + "relatedDatabasePlaceholder": "काही नाही", + "inRelatedDatabase": "या मध्ये", + "rowSearchTextFieldPlaceholder": "शोध", + "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:", + "emptySearchResult": "कोणतीही नोंद सापडली नाही", + "linkedRowListLabel": "{count} लिंक केलेल्या पंक्ती", + "unlinkedRowListLabel": "आणखी एक पंक्ती लिंक करा" +}, +"menuName": "ग्रिड", +"referencedGridPrefix": "दृश्य", +"calculate": "गणना करा", +"calculationTypeLabel": { + "none": "काही नाही", + "average": "सरासरी", + "max": "कमाल", + "median": "मध्यम", + "min": "किमान", + "sum": "बेरीज", + "count": "मोजणी", + "countEmpty": "रिकाम्यांची मोजणी", + "countEmptyShort": "रिक्त", + "countNonEmpty": "रिक्त नसलेल्यांची मोजणी", + "countNonEmptyShort": "भरलेले" +}, +"media": { + "rename": "पुन्हा नाव द्या", + "download": "डाउनलोड करा", + "expand": "मोठे करा", + "delete": "हटवा", + "moreFilesHint": "+{}", + "addFileOrImage": "फाईल किंवा लिंक जोडा", + "attachmentsHint": "{}", + "addFileMobile": "फाईल जोडा", + "extraCount": "+{}", + "deleteFileDescription": "तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", + "showFileNames": "फाईलचे नाव दाखवा", + "downloadSuccess": "फाईल डाउनलोड झाली", + "downloadFailedToken": "फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध", + "setAsCover": "कव्हर म्हणून सेट करा", + "openInBrowser": "ब्राउझरमध्ये उघडा", + "embedLink": "फाईल लिंक एम्बेड करा" + } +}, + "document": { + "menuName": "दस्तऐवज", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "creating": "तयार करत आहे...", + "slashMenu": { + "board": { + "selectABoardToLinkTo": "लिंक करण्यासाठी बोर्ड निवडा", + "createANewBoard": "नवीन बोर्ड तयार करा" + }, + "grid": { + "selectAGridToLinkTo": "लिंक करण्यासाठी ग्रिड निवडा", + "createANewGrid": "नवीन ग्रिड तयार करा" + }, + "calendar": { + "selectACalendarToLinkTo": "लिंक करण्यासाठी दिनदर्शिका निवडा", + "createANewCalendar": "नवीन दिनदर्शिका तयार करा" + }, + "document": { + "selectADocumentToLinkTo": "लिंक करण्यासाठी दस्तऐवज निवडा" + }, + "name": { + "textStyle": "मजकुराची शैली", + "list": "यादी", + "toggle": "टॉगल", + "fileAndMedia": "फाईल व मीडिया", + "simpleTable": "सोपे टेबल", + "visuals": "दृश्य घटक", + "document": "दस्तऐवज", + "advanced": "प्रगत", + "text": "मजकूर", + "heading1": "शीर्षक 1", + "heading2": "शीर्षक 2", + "heading3": "शीर्षक 3", + "image": "प्रतिमा", + "bulletedList": "बुलेट यादी", + "numberedList": "क्रमांकित यादी", + "todoList": "करण्याची यादी", + "doc": "दस्तऐवज", + "linkedDoc": "पृष्ठाशी लिंक करा", + "grid": "ग्रिड", + "linkedGrid": "लिंक केलेला ग्रिड", + "kanban": "कानबन", + "linkedKanban": "लिंक केलेला कानबन", + "calendar": "दिनदर्शिका", + "linkedCalendar": "लिंक केलेली दिनदर्शिका", + "quote": "उद्धरण", + "divider": "विभाजक", + "table": "टेबल", + "callout": "महत्त्वाचा मजकूर", + "outline": "रूपरेषा", + "mathEquation": "गणिती समीकरण", + "code": "कोड", + "toggleList": "टॉगल यादी", + "toggleHeading1": "टॉगल शीर्षक 1", + "toggleHeading2": "टॉगल शीर्षक 2", + "toggleHeading3": "टॉगल शीर्षक 3", + "emoji": "इमोजी", + "aiWriter": "AI ला काहीही विचारा", + "dateOrReminder": "दिनांक किंवा स्मरणपत्र", + "photoGallery": "फोटो गॅलरी", + "file": "फाईल", + "twoColumns": "२ स्तंभ", + "threeColumns": "३ स्तंभ", + "fourColumns": "४ स्तंभ" + }, + "subPage": { + "name": "दस्तऐवज", + "keyword1": "उपपृष्ठ", + "keyword2": "पृष्ठ", + "keyword3": "चाइल्ड पृष्ठ", + "keyword4": "पृष्ठ जोडा", + "keyword5": "एम्बेड पृष्ठ", + "keyword6": "नवीन पृष्ठ", + "keyword7": "पृष्ठ तयार करा", + "keyword8": "दस्तऐवज" + } + }, + "selectionMenu": { + "outline": "रूपरेषा", + "codeBlock": "कोड ब्लॉक" + }, + "plugins": { + "referencedBoard": "संदर्भित बोर्ड", + "referencedGrid": "संदर्भित ग्रिड", + "referencedCalendar": "संदर्भित दिनदर्शिका", + "referencedDocument": "संदर्भित दस्तऐवज", + "aiWriter": { + "userQuestion": "AI ला काहीही विचारा", + "continueWriting": "लेखन सुरू ठेवा", + "fixSpelling": "स्पेलिंग व व्याकरण सुधारणा", + "improveWriting": "लेखन सुधारित करा", + "summarize": "सारांश द्या", + "explain": "स्पष्टीकरण द्या", + "makeShorter": "लहान करा", + "makeLonger": "मोठे करा" + }, + "autoGeneratorMenuItemName": "AI लेखक", +"autoGeneratorTitleName": "AI: काहीही लिहिण्यासाठी AI ला विचारा...", +"autoGeneratorLearnMore": "अधिक जाणून घ्या", +"autoGeneratorGenerate": "उत्पन्न करा", +"autoGeneratorHintText": "AI ला विचारा...", +"autoGeneratorCantGetOpenAIKey": "AI की मिळवू शकलो नाही", +"autoGeneratorRewrite": "पुन्हा लिहा", +"smartEdit": "AI ला विचारा", +"aI": "AI", +"smartEditFixSpelling": "स्पेलिंग आणि व्याकरण सुधारा", +"warning": "⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.", +"smartEditSummarize": "सारांश द्या", +"smartEditImproveWriting": "लेखन सुधारित करा", +"smartEditMakeLonger": "लांब करा", +"smartEditCouldNotFetchResult": "AI कडून उत्तर मिळवता आले नाही", +"smartEditCouldNotFetchKey": "AI की मिळवता आली नाही", +"smartEditDisabled": "सेटिंग्जमध्ये AI कनेक्ट करा", +"appflowyAIEditDisabled": "AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा", +"discardResponse": "AI उत्तर फेकून द्यायचं आहे का?", +"createInlineMathEquation": "समीकरण तयार करा", +"fonts": "फॉन्ट्स", +"insertDate": "तारीख जोडा", +"emoji": "इमोजी", +"toggleList": "टॉगल यादी", +"emptyToggleHeading": "रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.", +"emptyToggleList": "रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.", +"emptyToggleHeadingWeb": "रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा", +"quoteList": "उद्धरण यादी", +"numberedList": "क्रमांकित यादी", +"bulletedList": "बुलेट यादी", +"todoList": "करण्याची यादी", +"callout": "ठळक मजकूर", +"simpleTable": { + "moreActions": { + "color": "रंग", + "align": "पंक्तिबद्ध करा", + "delete": "हटा", + "duplicate": "डुप्लिकेट करा", + "insertLeft": "डावीकडे घाला", + "insertRight": "उजवीकडे घाला", + "insertAbove": "वर घाला", + "insertBelow": "खाली घाला", + "headerColumn": "हेडर स्तंभ", + "headerRow": "हेडर ओळ", + "clearContents": "सामग्री साफ करा", + "setToPageWidth": "पृष्ठाच्या रुंदीप्रमाणे सेट करा", + "distributeColumnsWidth": "स्तंभ समान करा", + "duplicateRow": "ओळ डुप्लिकेट करा", + "duplicateColumn": "स्तंभ डुप्लिकेट करा", + "textColor": "मजकूराचा रंग", + "cellBackgroundColor": "सेलचा पार्श्वभूमी रंग", + "duplicateTable": "टेबल डुप्लिकेट करा" + }, + "clickToAddNewRow": "नवीन ओळ जोडण्यासाठी क्लिक करा", + "clickToAddNewColumn": "नवीन स्तंभ जोडण्यासाठी क्लिक करा", + "clickToAddNewRowAndColumn": "नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा", + "headerName": { + "table": "टेबल", + "alignText": "मजकूर पंक्तिबद्ध करा" + } +}, +"cover": { + "changeCover": "कव्हर बदला", + "colors": "रंग", + "images": "प्रतिमा", + "clearAll": "सर्व साफ करा", + "abstract": "ऍबस्ट्रॅक्ट", + "addCover": "कव्हर जोडा", + "addLocalImage": "स्थानिक प्रतिमा जोडा", + "invalidImageUrl": "अवैध प्रतिमा URL", + "failedToAddImageToGallery": "प्रतिमा गॅलरीत जोडता आली नाही", + "enterImageUrl": "प्रतिमा URL लिहा", + "add": "जोडा", + "back": "मागे", + "saveToGallery": "गॅलरीत जतन करा", + "removeIcon": "आयकॉन काढा", + "removeCover": "कव्हर काढा", + "pasteImageUrl": "प्रतिमा URL पेस्ट करा", + "or": "किंवा", + "pickFromFiles": "फाईल्समधून निवडा", + "couldNotFetchImage": "प्रतिमा मिळवता आली नाही", + "imageSavingFailed": "प्रतिमा जतन करणे अयशस्वी", + "addIcon": "आयकॉन जोडा", + "changeIcon": "आयकॉन बदला", + "coverRemoveAlert": "हे हटवल्यानंतर कव्हरमधून काढले जाईल.", + "alertDialogConfirmation": "तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?" +}, +"mathEquation": { + "name": "गणिती समीकरण", + "addMathEquation": "TeX समीकरण जोडा", + "editMathEquation": "गणिती समीकरण संपादित करा" +}, +"optionAction": { + "click": "क्लिक", + "toOpenMenu": "मेनू उघडण्यासाठी", + "drag": "ओढा", + "toMove": "हलवण्यासाठी", + "delete": "हटा", + "duplicate": "डुप्लिकेट करा", + "turnInto": "मध्ये बदला", + "moveUp": "वर हलवा", + "moveDown": "खाली हलवा", + "color": "रंग", + "align": "पंक्तिबद्ध करा", + "left": "डावीकडे", + "center": "मध्यभागी", + "right": "उजवीकडे", + "defaultColor": "डिफॉल्ट", + "depth": "खोली", + "copyLinkToBlock": "ब्लॉकसाठी लिंक कॉपी करा" +}, + "image": { + "addAnImage": "प्रतिमा जोडा", + "copiedToPasteBoard": "प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "addAnImageDesktop": "प्रतिमा जोडा", + "addAnImageMobile": "एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा", + "dropImageToInsert": "प्रतिमा ड्रॉप करून जोडा", + "imageUploadFailed": "प्रतिमा अपलोड करण्यात अयशस्वी", + "imageDownloadFailed": "प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "imageDownloadFailedToken": "युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "errorCode": "त्रुटी कोड" +}, +"photoGallery": { + "name": "फोटो गॅलरी", + "imageKeyword": "प्रतिमा", + "imageGalleryKeyword": "प्रतिमा गॅलरी", + "photoKeyword": "फोटो", + "photoBrowserKeyword": "फोटो ब्राउझर", + "galleryKeyword": "गॅलरी", + "addImageTooltip": "प्रतिमा जोडा", + "changeLayoutTooltip": "लेआउट बदला", + "browserLayout": "ब्राउझर", + "gridLayout": "ग्रिड", + "deleteBlockTooltip": "संपूर्ण गॅलरी हटवा" +}, +"math": { + "copiedToPasteBoard": "समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे" +}, +"urlPreview": { + "copiedToPasteBoard": "लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "convertToLink": "एंबेड लिंकमध्ये रूपांतर करा" +}, +"outline": { + "addHeadingToCreateOutline": "सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.", + "noMatchHeadings": "जुळणारी शीर्षके आढळली नाहीत." +}, +"table": { + "addAfter": "नंतर जोडा", + "addBefore": "आधी जोडा", + "delete": "हटा", + "clear": "सामग्री साफ करा", + "duplicate": "डुप्लिकेट करा", + "bgColor": "पार्श्वभूमीचा रंग" +}, +"contextMenu": { + "copy": "कॉपी करा", + "cut": "कापा", + "paste": "पेस्ट करा", + "pasteAsPlainText": "साध्या मजकूराच्या स्वरूपात पेस्ट करा" +}, +"action": "कृती", +"database": { + "selectDataSource": "डेटा स्रोत निवडा", + "noDataSource": "डेटा स्रोत नाही", + "selectADataSource": "डेटा स्रोत निवडा", + "toContinue": "पुढे जाण्यासाठी", + "newDatabase": "नवीन डेटाबेस", + "linkToDatabase": "डेटाबेसशी लिंक करा" +}, +"date": "तारीख", +"video": { + "label": "व्हिडिओ", + "emptyLabel": "व्हिडिओ जोडा", + "placeholder": "व्हिडिओ लिंक पेस्ट करा", + "copiedToPasteBoard": "व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "insertVideo": "व्हिडिओ जोडा", + "invalidVideoUrl": "ही URL सध्या समर्थित नाही.", + "invalidVideoUrlYouTube": "YouTube सध्या समर्थित नाही.", + "supportedFormats": "समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" +}, +"file": { + "name": "फाईल", + "uploadTab": "अपलोड", + "uploadMobile": "फाईल निवडा", + "uploadMobileGallery": "फोटो गॅलरीमधून", + "networkTab": "लिंक एम्बेड करा", + "placeholderText": "फाईल अपलोड किंवा एम्बेड करा", + "placeholderDragging": "फाईल ड्रॉप करून अपलोड करा", + "dropFileToUpload": "फाईल ड्रॉप करून अपलोड करा", + "fileUploadHint": "फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा ", + "fileUploadHintSuffix": "ब्राउझ करा", + "networkHint": "फाईल लिंक पेस्ट करा", + "networkUrlInvalid": "अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.", + "networkAction": "एम्बेड", + "fileTooBigError": "फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा", + "renameFile": { + "title": "फाईलचे नाव बदला", + "description": "या फाईलसाठी नवीन नाव लिहा", + "nameEmptyError": "फाईलचे नाव रिकामे असू शकत नाही." + }, + "uploadedAt": "{} रोजी अपलोड केले", + "linkedAt": "{} रोजी लिंक जोडली", + "failedToOpenMsg": "उघडण्यात अयशस्वी, फाईल सापडली नाही" +}, +"subPage": { + "handlingPasteHint": " - (पेस्ट प्रक्रिया करत आहे)", + "errors": { + "failedDeletePage": "पृष्ठ हटवण्यात अयशस्वी", + "failedCreatePage": "पृष्ठ तयार करण्यात अयशस्वी", + "failedMovePage": "हे पृष्ठ हलवण्यात अयशस्वी", + "failedDuplicatePage": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी", + "failedDuplicateFindView": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही" + } +}, + "cannotMoveToItsChildren": "मुलांमध्ये हलवू शकत नाही" +}, +"outlineBlock": { + "placeholder": "सामग्री सूची" +}, +"textBlock": { + "placeholder": "कमांडसाठी '/' टाइप करा" +}, +"title": { + "placeholder": "शीर्षक नाही" +}, +"imageBlock": { + "placeholder": "प्रतिमा जोडण्यासाठी क्लिक करा", + "upload": { + "label": "अपलोड", + "placeholder": "प्रतिमा अपलोड करण्यासाठी क्लिक करा" + }, + "url": { + "label": "प्रतिमेची URL", + "placeholder": "प्रतिमेची URL टाका" + }, + "ai": { + "label": "AI द्वारे प्रतिमा तयार करा", + "placeholder": "AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" + }, + "stability_ai": { + "label": "Stability AI द्वारे प्रतिमा तयार करा", + "placeholder": "Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" + }, + "support": "प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG", + "error": { + "invalidImage": "अवैध प्रतिमा", + "invalidImageSize": "प्रतिमेचा आकार 5MB पेक्षा कमी असावा", + "invalidImageFormat": "प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "अवैध प्रतिमेची URL", + "noImage": "अशी फाईल किंवा निर्देशिका नाही", + "multipleImagesFailed": "एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा" + }, + "embedLink": { + "label": "लिंक एम्बेड करा", + "placeholder": "प्रतिमेची लिंक पेस्ट करा किंवा टाका" + }, + "unsplash": { + "label": "Unsplash" + }, + "searchForAnImage": "प्रतिमा शोधा", + "pleaseInputYourOpenAIKey": "कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा", + "saveImageToGallery": "प्रतिमा जतन करा", + "failedToAddImageToGallery": "प्रतिमा जतन करण्यात अयशस्वी", + "successToAddImageToGallery": "प्रतिमा 'Photos' मध्ये जतन केली", + "unableToLoadImage": "प्रतिमा लोड करण्यात अयशस्वी", + "maximumImageSize": "कमाल प्रतिमा अपलोड आकार 10MB आहे", + "uploadImageErrorImageSizeTooBig": "प्रतिमेचा आकार 10MB पेक्षा कमी असावा", + "imageIsUploading": "प्रतिमा अपलोड होत आहे", + "openFullScreen": "पूर्ण स्क्रीनमध्ये उघडा", + "interactiveViewer": { + "toolbar": { + "previousImageTooltip": "मागील प्रतिमा", + "nextImageTooltip": "पुढील प्रतिमा", + "zoomOutTooltip": "लहान करा", + "zoomInTooltip": "मोठी करा", + "changeZoomLevelTooltip": "झूम पातळी बदला", + "openLocalImage": "प्रतिमा उघडा", + "downloadImage": "प्रतिमा डाउनलोड करा", + "closeViewer": "इंटरअॅक्टिव्ह व्ह्युअर बंद करा", + "scalePercentage": "{}%", + "deleteImageTooltip": "प्रतिमा हटवा" + } + } +}, + "codeBlock": { + "language": { + "label": "भाषा", + "placeholder": "भाषा निवडा", + "auto": "स्वयंचलित" + }, + "copyTooltip": "कॉपी करा", + "searchLanguageHint": "भाषा शोधा", + "codeCopiedSnackbar": "कोड क्लिपबोर्डवर कॉपी झाला!" +}, +"inlineLink": { + "placeholder": "लिंक पेस्ट करा किंवा टाका", + "openInNewTab": "नवीन टॅबमध्ये उघडा", + "copyLink": "लिंक कॉपी करा", + "removeLink": "लिंक काढा", + "url": { + "label": "लिंक URL", + "placeholder": "लिंक URL टाका" + }, + "title": { + "label": "लिंक शीर्षक", + "placeholder": "लिंक शीर्षक टाका" + } +}, +"mention": { + "placeholder": "कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...", + "page": { + "label": "पृष्ठाला लिंक करा", + "tooltip": "पृष्ठ उघडण्यासाठी क्लिक करा" + }, + "deleted": "हटवले गेले", + "deletedContent": "ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे", + "noAccess": "प्रवेश नाही", + "deletedPage": "हटवलेले पृष्ठ", + "trashHint": " - ट्रॅशमध्ये", + "morePages": "अजून पृष्ठे" +}, +"toolbar": { + "resetToDefaultFont": "डीफॉल्ट फॉन्टवर परत जा", + "textSize": "मजकूराचा आकार", + "textColor": "मजकूराचा रंग", + "h1": "मथळा 1", + "h2": "मथळा 2", + "h3": "मथळा 3", + "alignLeft": "डावीकडे संरेखित करा", + "alignRight": "उजवीकडे संरेखित करा", + "alignCenter": "मध्यभागी संरेखित करा", + "link": "लिंक", + "textAlign": "मजकूर संरेखन", + "moreOptions": "अधिक पर्याय", + "font": "फॉन्ट", + "inlineCode": "इनलाइन कोड", + "suggestions": "सूचना", + "turnInto": "मध्ये रूपांतरित करा", + "equation": "समीकरण", + "insert": "घाला", + "linkInputHint": "लिंक पेस्ट करा किंवा पृष्ठे शोधा", + "pageOrURL": "पृष्ठ किंवा URL", + "linkName": "लिंकचे नाव", + "linkNameHint": "लिंकचे नाव प्रविष्ट करा" +}, +"errorBlock": { + "theBlockIsNotSupported": "ब्लॉक सामग्री पार्स करण्यात अक्षम", + "clickToCopyTheBlockContent": "ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा", + "blockContentHasBeenCopied": "ब्लॉक सामग्री कॉपी केली आहे.", + "parseError": "{} ब्लॉक पार्स करताना त्रुटी आली.", + "copyBlockContent": "ब्लॉक सामग्री कॉपी करा" +}, +"mobilePageSelector": { + "title": "पृष्ठ निवडा", + "failedToLoad": "पृष्ठ यादी लोड करण्यात अयशस्वी", + "noPagesFound": "कोणतीही पृष्ठे सापडली नाहीत" +}, +"attachmentMenu": { + "choosePhoto": "फोटो निवडा", + "takePicture": "फोटो काढा", + "chooseFile": "फाईल निवडा" + } + }, + "board": { + "column": { + "label": "स्तंभ", + "createNewCard": "नवीन", + "renameGroupTooltip": "गटाचे नाव बदलण्यासाठी क्लिक करा", + "createNewColumn": "नवीन गट जोडा", + "addToColumnTopTooltip": "वर नवीन कार्ड जोडा", + "addToColumnBottomTooltip": "खाली नवीन कार्ड जोडा", + "renameColumn": "स्तंभाचे नाव बदला", + "hideColumn": "लपवा", + "newGroup": "नवीन गट", + "deleteColumn": "हटवा", + "deleteColumnConfirmation": "हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?" + }, + "hiddenGroupSection": { + "sectionTitle": "लपवलेले गट", + "collapseTooltip": "लपवलेले गट लपवा", + "expandTooltip": "लपवलेले गट पाहा" + }, + "cardDetail": "कार्ड तपशील", + "cardActions": "कार्ड क्रिया", + "cardDuplicated": "कार्डची प्रत तयार झाली", + "cardDeleted": "कार्ड हटवले गेले", + "showOnCard": "कार्ड तपशिलावर दाखवा", + "setting": "सेटिंग", + "propertyName": "गुणधर्माचे नाव", + "menuName": "बोर्ड", + "showUngrouped": "गटात नसलेली कार्ड्स दाखवा", + "ungroupedButtonText": "गट नसलेली", + "ungroupedButtonTooltip": "ज्या कार्ड्स कोणत्याही गटात नाहीत", + "ungroupedItemsTitle": "बोर्डमध्ये जोडण्यासाठी क्लिक करा", + "groupBy": "या आधारावर गट करा", + "groupCondition": "गट स्थिती", + "referencedBoardPrefix": "याचे दृश्य", + "notesTooltip": "नोट्स आहेत", + "mobile": { + "editURL": "URL संपादित करा", + "showGroup": "गट दाखवा", + "showGroupContent": "हा गट बोर्डवर दाखवायचा आहे का?", + "failedToLoad": "बोर्ड दृश्य लोड होण्यात अयशस्वी" + }, + "dateCondition": { + "weekOf": "{} - {} ची आठवडा", + "today": "आज", + "yesterday": "काल", + "tomorrow": "उद्या", + "lastSevenDays": "शेवटचे ७ दिवस", + "nextSevenDays": "पुढील ७ दिवस", + "lastThirtyDays": "शेवटचे ३० दिवस", + "nextThirtyDays": "पुढील ३० दिवस" + }, + "noGroup": "गटासाठी कोणताही गुणधर्म निवडलेला नाही", + "noGroupDesc": "बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे", + "media": { + "cardText": "{} {}", + "fallbackName": "फायली" + } +}, + "calendar": { + "menuName": "कॅलेंडर", + "defaultNewCalendarTitle": "नाव नाही", + "newEventButtonTooltip": "नवीन इव्हेंट जोडा", + "navigation": { + "today": "आज", + "jumpToday": "आजवर जा", + "previousMonth": "मागील महिना", + "nextMonth": "पुढील महिना", + "views": { + "day": "दिवस", + "week": "आठवडा", + "month": "महिना", + "year": "वर्ष" + } + }, + "mobileEventScreen": { + "emptyTitle": "सध्या कोणतेही इव्हेंट नाहीत", + "emptyBody": "या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा." + }, + "settings": { + "showWeekNumbers": "आठवड्याचे क्रमांक दाखवा", + "showWeekends": "सप्ताहांत दाखवा", + "firstDayOfWeek": "आठवड्याची सुरुवात", + "layoutDateField": "कॅलेंडर मांडणी दिनांकानुसार", + "changeLayoutDateField": "मांडणी फील्ड बदला", + "noDateTitle": "तारीख नाही", + "noDateHint": { + "zero": "नियोजित नसलेली इव्हेंट्स येथे दिसतील", + "one": "{count} नियोजित नसलेली इव्हेंट", + "other": "{count} नियोजित नसलेल्या इव्हेंट्स" + }, + "unscheduledEventsTitle": "नियोजित नसलेल्या इव्हेंट्स", + "clickToAdd": "कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा", + "name": "कॅलेंडर सेटिंग्ज", + "clickToOpen": "रेकॉर्ड उघडण्यासाठी क्लिक करा" + }, + "referencedCalendarPrefix": "याचे दृश्य", + "quickJumpYear": "या वर्षावर जा", + "duplicateEvent": "इव्हेंट डुप्लिकेट करा" +}, + "errorDialog": { + "title": "@:appName त्रुटी", + "howToFixFallback": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.", + "howToFixFallbackHint1": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया ", + "howToFixFallbackHint2": " पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.", + "github": "GitHub वर पहा" +}, +"search": { + "label": "शोध", + "sidebarSearchIcon": "पृष्ठ शोधा आणि पटकन जा", + "placeholder": { + "actions": "कृती शोधा..." + } +}, +"message": { + "copy": { + "success": "कॉपी झाले!", + "fail": "कॉपी करू शकत नाही" + } +}, +"unSupportBlock": "सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.", +"views": { + "deleteContentTitle": "तुम्हाला हे {pageType} हटवायचे आहे का?", + "deleteContentCaption": "हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता." +}, + "colors": { + "custom": "सानुकूल", + "default": "डीफॉल्ट", + "red": "लाल", + "orange": "संत्रा", + "yellow": "पिवळा", + "green": "हिरवा", + "blue": "निळा", + "purple": "जांभळा", + "pink": "गुलाबी", + "brown": "तपकिरी", + "gray": "करड्या रंगाचा" +}, + "emoji": { + "emojiTab": "इमोजी", + "search": "इमोजी शोधा", + "noRecent": "अलीकडील कोणतेही इमोजी नाहीत", + "noEmojiFound": "कोणतेही इमोजी सापडले नाहीत", + "filter": "फिल्टर", + "random": "योगायोगाने", + "selectSkinTone": "त्वचेचा टोन निवडा", + "remove": "इमोजी काढा", + "categories": { + "smileys": "स्मायली आणि भावना", + "people": "लोक", + "animals": "प्राणी आणि निसर्ग", + "food": "अन्न", + "activities": "क्रिया", + "places": "स्थळे", + "objects": "वस्तू", + "symbols": "चिन्हे", + "flags": "ध्वज", + "nature": "निसर्ग", + "frequentlyUsed": "नेहमी वापरलेले" + }, + "skinTone": { + "default": "डीफॉल्ट", + "light": "हलका", + "mediumLight": "मध्यम-हलका", + "medium": "मध्यम", + "mediumDark": "मध्यम-गडद", + "dark": "गडद" + }, + "openSourceIconsFrom": "खुल्या स्रोताचे आयकॉन्स" +}, + "inlineActions": { + "noResults": "निकाल नाही", + "recentPages": "अलीकडील पृष्ठे", + "pageReference": "पृष्ठ संदर्भ", + "docReference": "दस्तऐवज संदर्भ", + "boardReference": "बोर्ड संदर्भ", + "calReference": "कॅलेंडर संदर्भ", + "gridReference": "ग्रिड संदर्भ", + "date": "तारीख", + "reminder": { + "groupTitle": "स्मरणपत्र", + "shortKeyword": "remind" + }, + "createPage": "\"{}\" उप-पृष्ठ तयार करा" +}, + "datePicker": { + "dateTimeFormatTooltip": "सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला", + "dateFormat": "तारीख फॉरमॅट", + "includeTime": "वेळ समाविष्ट करा", + "isRange": "शेवटची तारीख", + "timeFormat": "वेळ फॉरमॅट", + "clearDate": "तारीख साफ करा", + "reminderLabel": "स्मरणपत्र", + "selectReminder": "स्मरणपत्र निवडा", + "reminderOptions": { + "none": "काहीही नाही", + "atTimeOfEvent": "इव्हेंटच्या वेळी", + "fiveMinsBefore": "५ मिनिटे आधी", + "tenMinsBefore": "१० मिनिटे आधी", + "fifteenMinsBefore": "१५ मिनिटे आधी", + "thirtyMinsBefore": "३० मिनिटे आधी", + "oneHourBefore": "१ तास आधी", + "twoHoursBefore": "२ तास आधी", + "onDayOfEvent": "इव्हेंटच्या दिवशी", + "oneDayBefore": "१ दिवस आधी", + "twoDaysBefore": "२ दिवस आधी", + "oneWeekBefore": "१ आठवडा आधी", + "custom": "सानुकूल" + } +}, + "relativeDates": { + "yesterday": "काल", + "today": "आज", + "tomorrow": "उद्या", + "oneWeek": "१ आठवडा" +}, + "notificationHub": { + "title": "सूचना", + "mobile": { + "title": "अपडेट्स" + }, + "emptyTitle": "सर्व पूर्ण झाले!", + "emptyBody": "कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.", + "tabs": { + "inbox": "इनबॉक्स", + "upcoming": "आगामी" + }, + "actions": { + "markAllRead": "सर्व वाचलेल्या म्हणून चिन्हित करा", + "showAll": "सर्व", + "showUnreads": "न वाचलेल्या" + }, + "filters": { + "ascending": "आरोही", + "descending": "अवरोही", + "groupByDate": "तारीखेनुसार गटबद्ध करा", + "showUnreadsOnly": "फक्त न वाचलेल्या दाखवा", + "resetToDefault": "डीफॉल्टवर रीसेट करा" + } +}, + "reminderNotification": { + "title": "स्मरणपत्र", + "message": "तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!", + "tooltipDelete": "हटवा", + "tooltipMarkRead": "वाचले म्हणून चिन्हित करा", + "tooltipMarkUnread": "न वाचले म्हणून चिन्हित करा" +}, + "findAndReplace": { + "find": "शोधा", + "previousMatch": "मागील जुळणारे", + "nextMatch": "पुढील जुळणारे", + "close": "बंद करा", + "replace": "बदला", + "replaceAll": "सर्व बदला", + "noResult": "कोणतेही निकाल नाहीत", + "caseSensitive": "केस सेंसिटिव्ह", + "searchMore": "अधिक निकालांसाठी शोधा" +}, + "error": { + "weAreSorry": "आम्ही क्षमस्व आहोत", + "loadingViewError": "हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.", + "syncError": "इतर डिव्हाइसमधून डेटा सिंक झाला नाही", + "syncErrorHint": "कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.", + "clickToCopy": "एरर कोड कॉपी करण्यासाठी क्लिक करा" +}, + "editor": { + "bold": "जाड", + "bulletedList": "बुलेट यादी", + "bulletedListShortForm": "बुलेट", + "checkbox": "चेकबॉक्स", + "embedCode": "कोड एम्बेड करा", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "हायलाइट", + "color": "रंग", + "image": "प्रतिमा", + "date": "तारीख", + "page": "पृष्ठ", + "italic": "तिरका", + "link": "लिंक", + "numberedList": "क्रमांकित यादी", + "numberedListShortForm": "क्रमांकित", + "toggleHeading1ShortForm": "Toggle H1", + "toggleHeading2ShortForm": "Toggle H2", + "toggleHeading3ShortForm": "Toggle H3", + "quote": "कोट", + "strikethrough": "ओढून टाका", + "text": "मजकूर", + "underline": "अधोरेखित", + "fontColorDefault": "डीफॉल्ट", + "fontColorGray": "धूसर", + "fontColorBrown": "तपकिरी", + "fontColorOrange": "केशरी", + "fontColorYellow": "पिवळा", + "fontColorGreen": "हिरवा", + "fontColorBlue": "निळा", + "fontColorPurple": "जांभळा", + "fontColorPink": "पिंग", + "fontColorRed": "लाल", + "backgroundColorDefault": "डीफॉल्ट पार्श्वभूमी", + "backgroundColorGray": "धूसर पार्श्वभूमी", + "backgroundColorBrown": "तपकिरी पार्श्वभूमी", + "backgroundColorOrange": "केशरी पार्श्वभूमी", + "backgroundColorYellow": "पिवळी पार्श्वभूमी", + "backgroundColorGreen": "हिरवी पार्श्वभूमी", + "backgroundColorBlue": "निळी पार्श्वभूमी", + "backgroundColorPurple": "जांभळी पार्श्वभूमी", + "backgroundColorPink": "पिंग पार्श्वभूमी", + "backgroundColorRed": "लाल पार्श्वभूमी", + "backgroundColorLime": "लिंबू पार्श्वभूमी", + "backgroundColorAqua": "पाण्याचा पार्श्वभूमी", + "done": "पूर्ण", + "cancel": "रद्द करा", + "tint1": "टिंट 1", + "tint2": "टिंट 2", + "tint3": "टिंट 3", + "tint4": "टिंट 4", + "tint5": "टिंट 5", + "tint6": "टिंट 6", + "tint7": "टिंट 7", + "tint8": "टिंट 8", + "tint9": "टिंट 9", + "lightLightTint1": "जांभळा", + "lightLightTint2": "पिंग", + "lightLightTint3": "फिकट पिंग", + "lightLightTint4": "केशरी", + "lightLightTint5": "पिवळा", + "lightLightTint6": "लिंबू", + "lightLightTint7": "हिरवा", + "lightLightTint8": "पाणी", + "lightLightTint9": "निळा", + "urlHint": "URL", + "mobileHeading1": "Heading 1", + "mobileHeading2": "Heading 2", + "mobileHeading3": "Heading 3", + "mobileHeading4": "Heading 4", + "mobileHeading5": "Heading 5", + "mobileHeading6": "Heading 6", + "textColor": "मजकूराचा रंग", + "backgroundColor": "पार्श्वभूमीचा रंग", + "addYourLink": "तुमची लिंक जोडा", + "openLink": "लिंक उघडा", + "copyLink": "लिंक कॉपी करा", + "removeLink": "लिंक काढा", + "editLink": "लिंक संपादित करा", + "linkText": "मजकूर", + "linkTextHint": "कृपया मजकूर प्रविष्ट करा", + "linkAddressHint": "कृपया URL प्रविष्ट करा", + "highlightColor": "हायलाइट रंग", + "clearHighlightColor": "हायलाइट काढा", + "customColor": "स्वतःचा रंग", + "hexValue": "Hex मूल्य", + "opacity": "अपारदर्शकता", + "resetToDefaultColor": "डीफॉल्ट रंगावर रीसेट करा", + "ltr": "LTR", + "rtl": "RTL", + "auto": "स्वयंचलित", + "cut": "कट", + "copy": "कॉपी", + "paste": "पेस्ट", + "find": "शोधा", + "select": "निवडा", + "selectAll": "सर्व निवडा", + "previousMatch": "मागील जुळणारे", + "nextMatch": "पुढील जुळणारे", + "closeFind": "बंद करा", + "replace": "बदला", + "replaceAll": "सर्व बदला", + "regex": "Regex", + "caseSensitive": "केस सेंसिटिव्ह", + "uploadImage": "प्रतिमा अपलोड करा", + "urlImage": "URL प्रतिमा", + "incorrectLink": "चुकीची लिंक", + "upload": "अपलोड", + "chooseImage": "प्रतिमा निवडा", + "loading": "लोड करत आहे", + "imageLoadFailed": "प्रतिमा लोड करण्यात अयशस्वी", + "divider": "विभाजक", + "table": "तक्त्याचे स्वरूप", + "colAddBefore": "यापूर्वी स्तंभ जोडा", + "rowAddBefore": "यापूर्वी पंक्ती जोडा", + "colAddAfter": "यानंतर स्तंभ जोडा", + "rowAddAfter": "यानंतर पंक्ती जोडा", + "colRemove": "स्तंभ काढा", + "rowRemove": "पंक्ती काढा", + "colDuplicate": "स्तंभ डुप्लिकेट", + "rowDuplicate": "पंक्ती डुप्लिकेट", + "colClear": "सामग्री साफ करा", + "rowClear": "सामग्री साफ करा", + "slashPlaceHolder": "'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा", + "typeSomething": "काहीतरी लिहा...", + "toggleListShortForm": "टॉगल", + "quoteListShortForm": "कोट", + "mathEquationShortForm": "सूत्र", + "codeBlockShortForm": "कोड" +}, + "favorite": { + "noFavorite": "कोणतेही आवडते पृष्ठ नाही", + "noFavoriteHintText": "पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा", + "removeFromSidebar": "साइडबारमधून काढा", + "addToSidebar": "साइडबारमध्ये पिन करा" +}, +"cardDetails": { + "notesPlaceholder": "/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा" +}, +"blockPlaceholders": { + "todoList": "करण्याची यादी", + "bulletList": "यादी", + "numberList": "क्रमांकित यादी", + "quote": "कोट", + "heading": "मथळा {}" +}, +"titleBar": { + "pageIcon": "पृष्ठ चिन्ह", + "language": "भाषा", + "font": "फॉन्ट", + "actions": "क्रिया", + "date": "तारीख", + "addField": "फील्ड जोडा", + "userIcon": "वापरकर्त्याचे चिन्ह" +}, +"noLogFiles": "कोणतीही लॉग फाइल्स नाहीत", +"newSettings": { + "myAccount": { + "title": "माझे खाते", + "subtitle": "तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.", + "profileLabel": "खाते नाव आणि प्रोफाइल चित्र", + "profileNamePlaceholder": "तुमचे नाव प्रविष्ट करा", + "accountSecurity": "खाते सुरक्षा", + "2FA": "2-स्टेप प्रमाणीकरण", + "aiKeys": "AI कीज", + "accountLogin": "खाते लॉगिन", + "updateNameError": "नाव अपडेट करण्यात अयशस्वी", + "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", + "aboutAppFlowy": "@:appName विषयी", + "deleteAccount": { + "title": "खाते हटवा", + "subtitle": "तुमचे खाते आणि सर्व डेटा कायमचे हटवा.", + "description": "तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.", + "deleteMyAccount": "माझे खाते हटवा", + "dialogTitle": "खाते हटवा", + "dialogContent1": "तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?", + "dialogContent2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.", + "confirmHint1": "कृपया पुष्टीसाठी \"@:newSettings.myAccount.deleteAccount.confirmHint3\" टाइप करा.", + "confirmHint2": "मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.", + "confirmHint3": "DELETE MY ACCOUNT", + "checkToConfirmError": "हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे", + "failedToGetCurrentUser": "वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी", + "confirmTextValidationFailed": "तुमचा पुष्टी मजकूर \"@:newSettings.myAccount.deleteAccount.confirmHint3\" शी जुळत नाही", + "deleteAccountSuccess": "खाते यशस्वीरित्या हटवले गेले" + } + }, + "workplace": { + "name": "वर्कस्पेस", + "title": "वर्कस्पेस सेटिंग्स", + "subtitle": "तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.", + "workplaceName": "वर्कस्पेसचे नाव", + "workplaceNamePlaceholder": "वर्कस्पेसचे नाव टाका", + "workplaceIcon": "वर्कस्पेस चिन्ह", + "workplaceIconSubtitle": "एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.", + "renameError": "वर्कस्पेसचे नाव बदलण्यात अयशस्वी", + "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", + "chooseAnIcon": "चिन्ह निवडा", + "appearance": { + "name": "दृश्यरूप", + "themeMode": { + "auto": "स्वयंचलित", + "light": "प्रकाश मोड", + "dark": "गडद मोड" + }, + "language": "भाषा" + } + }, + "syncState": { + "syncing": "सिंक्रोनायझ करत आहे", + "synced": "सिंक्रोनायझ झाले", + "noNetworkConnected": "नेटवर्क कनेक्ट केलेले नाही" + } +}, + "pageStyle": { + "title": "पृष्ठ शैली", + "layout": "लेआउट", + "coverImage": "मुखपृष्ठ प्रतिमा", + "pageIcon": "पृष्ठ चिन्ह", + "colors": "रंग", + "gradient": "ग्रेडियंट", + "backgroundImage": "पार्श्वभूमी प्रतिमा", + "presets": "पूर्वनियोजित", + "photo": "फोटो", + "unsplash": "Unsplash", + "pageCover": "पृष्ठ कव्हर", + "none": "काही नाही", + "openSettings": "सेटिंग्स उघडा", + "photoPermissionTitle": "@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे", + "photoPermissionDescription": "तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे", + "cameraPermissionTitle": "@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे", + "cameraPermissionDescription": "कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे", + "doNotAllow": "परवानगी देऊ नका", + "image": "प्रतिमा" +}, +"commandPalette": { + "placeholder": "शोधा किंवा प्रश्न विचारा...", + "bestMatches": "सर्वोत्तम जुळवणी", + "recentHistory": "अलीकडील इतिहास", + "navigateHint": "नेव्हिगेट करण्यासाठी", + "loadingTooltip": "आम्ही निकाल शोधत आहोत...", + "betaLabel": "बेटा", + "betaTooltip": "सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो", + "fromTrashHint": "कचरापेटीतून", + "noResultsHint": "आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.", + "clearSearchTooltip": "शोध फील्ड साफ करा" +}, +"space": { + "delete": "हटवा", + "deleteConfirmation": "हटवा: ", + "deleteConfirmationDescription": "या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.", + "rename": "स्पेसचे नाव बदला", + "changeIcon": "चिन्ह बदला", + "manage": "स्पेस व्यवस्थापित करा", + "addNewSpace": "स्पेस तयार करा", + "collapseAllSubPages": "सर्व उपपृष्ठे संकुचित करा", + "createNewSpace": "नवीन स्पेस तयार करा", + "createSpaceDescription": "तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.", + "spaceName": "स्पेसचे नाव", + "spaceNamePlaceholder": "उदा. मार्केटिंग, अभियांत्रिकी, HR", + "permission": "स्पेस परवानगी", + "publicPermission": "सार्वजनिक", + "publicPermissionDescription": "पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य", + "privatePermission": "खाजगी", + "privatePermissionDescription": "फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे", + "spaceIconBackground": "पार्श्वभूमीचा रंग", + "spaceIcon": "चिन्ह", + "dangerZone": "धोकादायक क्षेत्र", + "unableToDeleteLastSpace": "शेवटची स्पेस हटवता येणार नाही", + "unableToDeleteSpaceNotCreatedByYou": "तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही", + "enableSpacesForYourWorkspace": "तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा", + "title": "स्पेसेस", + "defaultSpaceName": "सामान्य", + "upgradeSpaceTitle": "स्पेस सक्षम करा", + "upgradeSpaceDescription": "तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.", + "upgrade": "अपग्रेड", + "upgradeYourSpace": "अनेक स्पेस तयार करा", + "quicklySwitch": "पुढील स्पेसवर पटकन स्विच करा", + "duplicate": "स्पेस डुप्लिकेट करा", + "movePageToSpace": "पृष्ठ स्पेसमध्ये हलवा", + "cannotMovePageToDatabase": "पृष्ठ डेटाबेसमध्ये हलवता येणार नाही", + "switchSpace": "स्पेस स्विच करा", + "spaceNameCannotBeEmpty": "स्पेसचे नाव रिकामे असू शकत नाही", + "success": { + "deleteSpace": "स्पेस यशस्वीरित्या हटवली", + "renameSpace": "स्पेसचे नाव यशस्वीरित्या बदलले", + "duplicateSpace": "स्पेस यशस्वीरित्या डुप्लिकेट केली", + "updateSpace": "स्पेस यशस्वीरित्या अपडेट केली" + }, + "error": { + "deleteSpace": "स्पेस हटवण्यात अयशस्वी", + "renameSpace": "स्पेसचे नाव बदलण्यात अयशस्वी", + "duplicateSpace": "स्पेस डुप्लिकेट करण्यात अयशस्वी", + "updateSpace": "स्पेस अपडेट करण्यात अयशस्वी" + }, + "createSpace": "स्पेस तयार करा", + "manageSpace": "स्पेस व्यवस्थापित करा", + "renameSpace": "स्पेसचे नाव बदला", + "mSpaceIconColor": "स्पेस चिन्हाचा रंग", + "mSpaceIcon": "स्पेस चिन्ह" +}, + "publish": { + "hasNotBeenPublished": "हे पृष्ठ अजून प्रकाशित केलेले नाही", + "spaceHasNotBeenPublished": "स्पेस प्रकाशित करण्यासाठी समर्थन नाही", + "reportPage": "पृष्ठाची तक्रार करा", + "databaseHasNotBeenPublished": "डेटाबेस प्रकाशित करण्यास समर्थन नाही.", + "createdWith": "यांनी तयार केले", + "downloadApp": "AppFlowy डाउनलोड करा", + "copy": { + "codeBlock": "कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे", + "imageBlock": "प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", + "mathBlock": "गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे", + "fileBlock": "फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे" + }, + "containsPublishedPage": "या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?", + "publishSuccessfully": "यशस्वीरित्या प्रकाशित झाले", + "unpublishSuccessfully": "यशस्वीरित्या अनपब्लिश झाले", + "publishFailed": "प्रकाशित करण्यात अयशस्वी", + "unpublishFailed": "अनपब्लिश करण्यात अयशस्वी", + "noAccessToVisit": "या पृष्ठावर प्रवेश नाही...", + "createWithAppFlowy": "AppFlowy ने वेबसाइट तयार करा", + "fastWithAI": "AI सह जलद आणि सोपे.", + "tryItNow": "आत्ताच वापरून पहा", + "onlyGridViewCanBePublished": "फक्त Grid view प्रकाशित केला जाऊ शकतो", + "database": { + "zero": "{} निवडलेले दृश्य प्रकाशित करा", + "one": "{} निवडलेली दृश्ये प्रकाशित करा", + "many": "{} निवडलेली दृश्ये प्रकाशित करा", + "other": "{} निवडलेली दृश्ये प्रकाशित करा" + }, + "mustSelectPrimaryDatabase": "प्राथमिक दृश्य निवडणे आवश्यक आहे", + "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.", + "unableToDeselectPrimaryDatabase": "प्राथमिक डेटाबेस अननिवड करता येणार नाही", + "saveThisPage": "या टेम्पलेटपासून सुरू करा", + "duplicateTitle": "तुम्हाला हे कुठे जोडायचे आहे", + "selectWorkspace": "वर्कस्पेस निवडा", + "addTo": "मध्ये जोडा", + "duplicateSuccessfully": "तुमच्या वर्कस्पेसमध्ये जोडले गेले", + "duplicateSuccessfullyDescription": "AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.", + "downloadIt": "डाउनलोड करा", + "openApp": "अ‍ॅपमध्ये उघडा", + "duplicateFailed": "डुप्लिकेट करण्यात अयशस्वी", + "membersCount": { + "zero": "सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "useThisTemplate": "हा टेम्पलेट वापरा" +}, +"web": { + "continue": "पुढे जा", + "or": "किंवा", + "continueWithGoogle": "Google सह पुढे जा", + "continueWithGithub": "GitHub सह पुढे जा", + "continueWithDiscord": "Discord सह पुढे जा", + "continueWithApple": "Apple सह पुढे जा", + "moreOptions": "अधिक पर्याय", + "collapse": "आकुंचन", + "signInAgreement": "\"पुढे जा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", + "signInLocalAgreement": "\"सुरुवात करा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", + "and": "आणि", + "termOfUse": "वापर अटी", + "privacyPolicy": "गोपनीयता धोरण", + "signInError": "साइन इन त्रुटी", + "login": "साइन अप किंवा लॉग इन करा", + "fileBlock": { + "uploadedAt": "{time} रोजी अपलोड केले", + "linkedAt": "{time} रोजी लिंक जोडली", + "empty": "फाईल अपलोड करा किंवा एम्बेड करा", + "uploadFailed": "अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा", + "retry": "पुन्हा प्रयत्न करा" + }, + "importNotion": "Notion वरून आयात करा", + "import": "आयात करा", + "importSuccess": "यशस्वीरित्या अपलोड केले", + "importSuccessMessage": "आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.", + "importFailed": "आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा", + "dropNotionFile": "तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा", + "error": { + "pageNameIsEmpty": "पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा" + } +}, + "globalComment": { + "comments": "टिप्पण्या", + "addComment": "टिप्पणी जोडा", + "reactedBy": "यांनी प्रतिक्रिया दिली", + "addReaction": "प्रतिक्रिया जोडा", + "reactedByMore": "आणि {count} इतर", + "showSeconds": { + "one": "1 सेकंदापूर्वी", + "other": "{count} सेकंदांपूर्वी", + "zero": "आत्ताच", + "many": "{count} सेकंदांपूर्वी" + }, + "showMinutes": { + "one": "1 मिनिटापूर्वी", + "other": "{count} मिनिटांपूर्वी", + "many": "{count} मिनिटांपूर्वी" + }, + "showHours": { + "one": "1 तासापूर्वी", + "other": "{count} तासांपूर्वी", + "many": "{count} तासांपूर्वी" + }, + "showDays": { + "one": "1 दिवसापूर्वी", + "other": "{count} दिवसांपूर्वी", + "many": "{count} दिवसांपूर्वी" + }, + "showMonths": { + "one": "1 महिन्यापूर्वी", + "other": "{count} महिन्यांपूर्वी", + "many": "{count} महिन्यांपूर्वी" + }, + "showYears": { + "one": "1 वर्षापूर्वी", + "other": "{count} वर्षांपूर्वी", + "many": "{count} वर्षांपूर्वी" + }, + "reply": "उत्तर द्या", + "deleteComment": "टिप्पणी हटवा", + "youAreNotOwner": "तुम्ही या टिप्पणीचे मालक नाही", + "confirmDeleteDescription": "तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?", + "hasBeenDeleted": "हटवले गेले", + "replyingTo": "याला उत्तर देत आहे", + "noAccessDeleteComment": "तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही", + "collapse": "संकुचित करा", + "readMore": "अधिक वाचा", + "failedToAddComment": "टिप्पणी जोडण्यात अयशस्वी", + "commentAddedSuccessfully": "टिप्पणी यशस्वीरित्या जोडली गेली.", + "commentAddedSuccessTip": "तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?" +}, + "template": { + "asTemplate": "टेम्पलेट म्हणून जतन करा", + "name": "टेम्पलेट नाव", + "description": "टेम्पलेट वर्णन", + "about": "टेम्पलेट माहिती", + "deleteFromTemplate": "टेम्पलेटमधून हटवा", + "preview": "टेम्पलेट पूर्वदृश्य", + "categories": "टेम्पलेट श्रेणी", + "isNewTemplate": "नवीन टेम्पलेटमध्ये पिन करा", + "featured": "वैशिष्ट्यीकृतमध्ये पिन करा", + "relatedTemplates": "संबंधित टेम्पलेट्स", + "requiredField": "{field} आवश्यक आहे", + "addCategory": "\"{category}\" जोडा", + "addNewCategory": "नवीन श्रेणी जोडा", + "addNewCreator": "नवीन निर्माता जोडा", + "deleteCategory": "श्रेणी हटवा", + "editCategory": "श्रेणी संपादित करा", + "editCreator": "निर्माता संपादित करा", + "category": { + "name": "श्रेणीचे नाव", + "icon": "श्रेणी चिन्ह", + "bgColor": "श्रेणी पार्श्वभूमीचा रंग", + "priority": "श्रेणी प्राधान्य", + "desc": "श्रेणीचे वर्णन", + "type": "श्रेणी प्रकार", + "icons": "श्रेणी चिन्हे", + "colors": "श्रेणी रंग", + "byUseCase": "वापराच्या आधारे", + "byFeature": "वैशिष्ट्यांनुसार", + "deleteCategory": "श्रेणी हटवा", + "deleteCategoryDescription": "तुम्हाला ही श्रेणी हटवायची आहे का?", + "typeToSearch": "श्रेणी शोधण्यासाठी टाइप करा..." + }, + "creator": { + "label": "टेम्पलेट निर्माता", + "name": "निर्मात्याचे नाव", + "avatar": "निर्मात्याचा अवतार", + "accountLinks": "निर्मात्याचे खाते दुवे", + "uploadAvatar": "अवतार अपलोड करण्यासाठी क्लिक करा", + "deleteCreator": "निर्माता हटवा", + "deleteCreatorDescription": "तुम्हाला हा निर्माता हटवायचा आहे का?", + "typeToSearch": "निर्माते शोधण्यासाठी टाइप करा..." + }, + "uploadSuccess": "टेम्पलेट यशस्वीरित्या अपलोड झाले", + "uploadSuccessDescription": "तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.", + "viewTemplate": "टेम्पलेट पहा", + "deleteTemplate": "टेम्पलेट हटवा", + "deleteSuccess": "टेम्पलेट यशस्वीरित्या हटवले गेले", + "deleteTemplateDescription": "याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?", + "addRelatedTemplate": "संबंधित टेम्पलेट जोडा", + "removeRelatedTemplate": "संबंधित टेम्पलेट हटवा", + "uploadAvatar": "अवतार अपलोड करा", + "searchInCategory": "{category} मध्ये शोधा", + "label": "टेम्पलेट्स" +}, + "fileDropzone": { + "dropFile": "फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा", + "uploading": "अपलोड करत आहे...", + "uploadFailed": "अपलोड अयशस्वी", + "uploadSuccess": "अपलोड यशस्वी", + "uploadSuccessDescription": "फाइल यशस्वीरित्या अपलोड झाली आहे", + "uploadFailedDescription": "फाइल अपलोड अयशस्वी झाली आहे", + "uploadingDescription": "फाइल अपलोड होत आहे" +}, + "gallery": { + "preview": "पूर्ण स्क्रीनमध्ये उघडा", + "copy": "कॉपी करा", + "download": "डाउनलोड", + "prev": "मागील", + "next": "पुढील", + "resetZoom": "झूम रिसेट करा", + "zoomIn": "झूम इन", + "zoomOut": "झूम आउट" +}, + "invitation": { + "join": "सामील व्हा", + "on": "वर", + "invitedBy": "यांनी आमंत्रित केले", + "membersCount": { + "zero": "{count} सदस्य", + "one": "{count} सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "tip": "तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.", + "joinWorkspace": "वर्कस्पेसमध्ये सामील व्हा", + "success": "तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात", + "successMessage": "आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.", + "openWorkspace": "AppFlowy उघडा", + "alreadyAccepted": "तुम्ही आधीच आमंत्रण स्वीकारले आहे", + "errorModal": { + "title": "काहीतरी चुकले आहे", + "description": "तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.", + "contactOwner": "मालकाशी संपर्क करा", + "close": "मुख्यपृष्ठावर परत जा", + "changeAccount": "खाते बदला" + } +}, + "requestAccess": { + "title": "या पृष्ठासाठी प्रवेश नाही", + "subtitle": "तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.", + "requestAccess": "प्रवेशाची विनंती करा", + "backToHome": "मुख्यपृष्ठावर परत जा", + "tip": "तुम्ही सध्या म्हणून लॉग इन आहात.", + "mightBe": "कदाचित तुम्हाला दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.", + "successful": "विनंती यशस्वीपणे पाठवली गेली", + "successfulMessage": "मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.", + "requestError": "प्रवेशाची विनंती अयशस्वी", + "repeatRequestError": "तुम्ही यासाठी आधीच विनंती केली आहे" +}, + "approveAccess": { + "title": "वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा", + "requestSummary": " यांनी मध्ये सामील होण्यासाठी आणि पाहण्यासाठी विनंती केली आहे", + "upgrade": "अपग्रेड", + "downloadApp": "AppFlowy डाउनलोड करा", + "approveButton": "मंजूर करा", + "approveSuccess": "मंजूर यशस्वी", + "approveError": "मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा", + "getRequestInfoError": "विनंतीची माहिती मिळवण्यात अयशस्वी", + "memberCount": { + "zero": "कोणतेही सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" + }, + "alreadyProTitle": "वर्कस्पेस योजना मर्यादा गाठली आहे", + "alreadyProMessage": "अधिक सदस्यांसाठी शी संपर्क साधा", + "repeatApproveError": "तुम्ही ही विनंती आधीच मंजूर केली आहे", + "ensurePlanLimit": "कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना करा किंवा करा.", + "requestToJoin": "मध्ये सामील होण्यासाठी विनंती केली", + "asMember": "सदस्य म्हणून" +}, + "upgradePlanModal": { + "title": "Pro प्लॅनवर अपग्रेड करा", + "message": "{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.", + "upgradeSteps": "AppFlowy वर तुमची योजना कशी अपग्रेड करावी:", + "step1": "1. सेटिंग्जमध्ये जा", + "step2": "2. 'योजना' वर क्लिक करा", + "step3": "3. 'योजना बदला' निवडा", + "appNote": "नोंद:", + "actionButton": "अपग्रेड करा", + "downloadLink": "अ‍ॅप डाउनलोड करा", + "laterButton": "नंतर", + "refreshNote": "यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी वर क्लिक करा.", + "refresh": "येथे" +}, + "breadcrumbs": { + "label": "ब्रेडक्रम्स" +}, + "time": { + "justNow": "आत्ताच", + "seconds": { + "one": "1 सेकंद", + "other": "{count} सेकंद" + }, + "minutes": { + "one": "1 मिनिट", + "other": "{count} मिनिटे" + }, + "hours": { + "one": "1 तास", + "other": "{count} तास" + }, + "days": { + "one": "1 दिवस", + "other": "{count} दिवस" + }, + "weeks": { + "one": "1 आठवडा", + "other": "{count} आठवडे" + }, + "months": { + "one": "1 महिना", + "other": "{count} महिने" + }, + "years": { + "one": "1 वर्ष", + "other": "{count} वर्षे" + }, + "ago": "पूर्वी", + "yesterday": "काल", + "today": "आज" +}, + "members": { + "zero": "सदस्य नाहीत", + "one": "1 सदस्य", + "many": "{count} सदस्य", + "other": "{count} सदस्य" +}, + "tabMenu": { + "close": "बंद करा", + "closeDisabledHint": "पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा", + "closeOthers": "इतर टॅब बंद करा", + "closeOthersHint": "हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता", + "closeOthersDisabledHint": "सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत", + "favorite": "आवडते", + "unfavorite": "आवडते काढा", + "favoriteDisabledHint": "हे दृश्य आवडते म्हणून जतन करता येत नाही", + "pinTab": "पिन करा", + "unpinTab": "अनपिन करा" +}, + "openFileMessage": { + "success": "फाइल यशस्वीरित्या उघडली", + "fileNotFound": "फाइल सापडली नाही", + "noAppToOpenFile": "ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही", + "permissionDenied": "ही फाइल उघडण्यासाठी परवानगी नाही", + "unknownError": "फाइल उघडण्यात अयशस्वी" +}, + "inviteMember": { + "requestInviteMembers": "तुमच्या वर्कस्पेसमध्ये आमंत्रित करा", + "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, कृपया ", + "upgrade": "अपग्रेड करा", + "addEmail": "email@example.com, email2@example.com...", + "requestInvites": "आमंत्रण पाठवा", + "inviteAlready": "तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}", + "inviteSuccess": "आमंत्रण यशस्वीपणे पाठवले", + "description": "खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.", + "emails": "ईमेल" +}, + "quickNote": { + "label": "झटपट नोंद", + "quickNotes": "झटपट नोंदी", + "search": "झटपट नोंदी शोधा", + "collapseFullView": "पूर्ण दृश्य लपवा", + "expandFullView": "पूर्ण दृश्य उघडा", + "createFailed": "झटपट नोंद तयार करण्यात अयशस्वी", + "quickNotesEmpty": "कोणत्याही झटपट नोंदी नाहीत", + "emptyNote": "रिकामी नोंद", + "deleteNotePrompt": "निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?", + "addNote": "नवीन नोंद", + "noAdditionalText": "अधिक माहिती नाही" +}, + "subscribe": { + "upgradePlanTitle": "योजना तुलना करा आणि निवडा", + "yearly": "वार्षिक", + "save": "{discount}% बचत", + "monthly": "मासिक", + "priceIn": "किंमत येथे: ", + "free": "फ्री", + "pro": "प्रो", + "freeDescription": "2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी", + "proDescription": "प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी", + "proDuration": { + "monthly": "प्रति सदस्य प्रति महिना\nमासिक बिलिंग", + "yearly": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग" + }, + "cancel": "खालच्या योजनेवर जा", + "changePlan": "प्रो योजनेवर अपग्रेड करा", + "everythingInFree": "फ्री योजनेतील सर्व काही +", + "currentPlan": "सध्याची योजना", + "freeDuration": "कायम", + "freePoints": { + "first": "1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)", + "second": "अमर्यादित पृष्ठे आणि ब्लॉक्स", + "three": "5 GB संचयन", + "four": "बुद्धिमान शोध", + "five": "20 AI प्रतिसाद", + "six": "मोबाईल अ‍ॅप", + "seven": "रिअल-टाइम सहकार्य" + }, + "proPoints": { + "first": "अमर्यादित संचयन", + "second": "10 वर्कस्पेस सदस्यांपर्यंत", + "three": "अमर्यादित AI प्रतिसाद", + "four": "अमर्यादित फाइल अपलोड्स", + "five": "कस्टम नेमस्पेस" + }, + "cancelPlan": { + "title": "आपल्याला जाताना पाहून वाईट वाटते", + "success": "आपली सदस्यता यशस्वीरित्या रद्द झाली आहे", + "description": "आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.", + "commonOther": "इतर", + "otherHint": "आपले उत्तर येथे लिहा", + "questionOne": { + "question": "तुम्ही AppFlowy Pro सदस्यता का रद्द केली?", + "answerOne": "खर्च खूप जास्त आहे", + "answerTwo": "वैशिष्ट्ये अपेक्षेनुसार नव्हती", + "answerThree": "चांगला पर्याय सापडला", + "answerFour": "खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता", + "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" + }, + "questionTwo": { + "question": "तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?", + "answerOne": "खूप शक्यता आहे", + "answerTwo": "काहीशी शक्यता आहे", + "answerThree": "निश्चित नाही", + "answerFour": "अल्प शक्यता आहे", + "answerFive": "शक्यता नाही" + }, + "questionThree": { + "question": "सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?", + "answerOne": "मल्टी-यूजर सहकार्य", + "answerTwo": "लांब कालावधीसाठी आवृत्ती इतिहास", + "answerThree": "अमर्यादित AI प्रतिसाद", + "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" + }, + "questionFour": { + "question": "AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?", + "answerOne": "छान", + "answerTwo": "चांगला", + "answerThree": "सामान्य", + "answerFour": "थोडासा वाईट", + "answerFive": "असंतोषजनक" + } + } +}, + "ai": { + "contentPolicyViolation": "संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.", + "textLimitReachedDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.", + "imageLimitReachedDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.", + "limitReachedAction": { + "textDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया", + "imageDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया", + "upgrade": "अपग्रेड करा", + "toThe": "या योजनेवर", + "proPlan": "प्रो योजना", + "orPurchaseAn": "किंवा खरेदी करा", + "aiAddon": "AI अ‍ॅड-ऑन" + }, + "editing": "संपादन करत आहे", + "analyzing": "विश्लेषण करत आहे", + "continueWritingEmptyDocumentTitle": "लेखन सुरू ठेवता आले नाही", + "continueWritingEmptyDocumentDescription": "तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!", + "more": "अधिक" +}, + "autoUpdate": { + "criticalUpdateTitle": "अद्यतन आवश्यक आहे", + "criticalUpdateDescription": "तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.", + "criticalUpdateButton": "अद्यतन करा", + "bannerUpdateTitle": "नवीन आवृत्ती उपलब्ध!", + "bannerUpdateDescription": "नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.", + "bannerUpdateButton": "अद्यतन करा", + "settingsUpdateTitle": "नवीन आवृत्ती ({newVersion}) उपलब्ध!", + "settingsUpdateDescription": "सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}", + "settingsUpdateButton": "अद्यतन करा", + "settingsUpdateWhatsNew": "काय नवीन आहे" +}, + "lockPage": { + "lockPage": "लॉक केलेले", + "reLockPage": "पुन्हा लॉक करा", + "lockTooltip": "अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.", + "pageLockedToast": "पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.", + "lockedOperationTooltip": "पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे." +}, + "suggestion": { + "accept": "स्वीकारा", + "keep": "जसे आहे तसे ठेवा", + "discard": "रद्द करा", + "close": "बंद करा", + "tryAgain": "पुन्हा प्रयत्न करा", + "rewrite": "पुन्हा लिहा", + "insertBelow": "खाली टाका" +} +} From 8d2901cc5814f7921802351bd602826abce7437d Mon Sep 17 00:00:00 2001 From: FakhriAzzouz Date: Wed, 9 Apr 2025 02:38:00 +0100 Subject: [PATCH 286/384] chore: arabic translations update (#7701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 8 ++++++-- frontend/resources/translations/ca-ES.json | 4 ++-- frontend/resources/translations/ckb-KU.json | 4 ++-- frontend/resources/translations/cs-CZ.json | 4 ++-- frontend/resources/translations/de-DE.json | 4 ++-- frontend/resources/translations/es-VE.json | 4 ++-- frontend/resources/translations/eu-ES.json | 4 ++-- frontend/resources/translations/fa.json | 4 ++-- frontend/resources/translations/fr-CA.json | 4 ++-- frontend/resources/translations/fr-FR.json | 4 ++-- frontend/resources/translations/he.json | 4 ++-- frontend/resources/translations/hu-HU.json | 4 ++-- frontend/resources/translations/id-ID.json | 4 ++-- frontend/resources/translations/it-IT.json | 4 ++-- frontend/resources/translations/ja-JP.json | 4 ++-- frontend/resources/translations/ko-KR.json | 4 ++-- frontend/resources/translations/pl-PL.json | 4 ++-- frontend/resources/translations/pt-BR.json | 4 ++-- frontend/resources/translations/pt-PT.json | 4 ++-- frontend/resources/translations/ru-RU.json | 4 ++-- frontend/resources/translations/sv-SE.json | 4 ++-- frontend/resources/translations/th-TH.json | 4 ++-- frontend/resources/translations/tr-TR.json | 4 ++-- frontend/resources/translations/uk-UA.json | 4 ++-- frontend/resources/translations/vi-VN.json | 4 ++-- frontend/resources/translations/zh-CN.json | 4 ++-- frontend/resources/translations/zh-TW.json | 4 ++-- 27 files changed, 58 insertions(+), 54 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 4afd35c40e..e8ca8c4ceb 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -311,14 +311,16 @@ "questionBubble": { "shortcuts": "الاختصارات", "whatsNew": "ما هو الجديد؟", - "help": "المساعدة والدعم", + "helpAndDocumentation": "المساعدة والتوثيق", + "getSupport": "احصل على الدعم", "markdown": "Markdown", "debug": { "name": "معلومات التصحيح", "success": "تم نسخ معلومات التصحيح إلى الحافظة!", "fail": "تعذر نسخ معلومات التصحيح إلى الحافظة" }, - "feedback": "تعليق" + "feedback": "تعليق", + "help": "المساعدة والدعم" }, "menuAppHeader": { "moreButtonToolTip": "إزالة وإعادة تسمية والمزيد...", @@ -513,6 +515,8 @@ "settings": "إعدادات", "members": "الأعضاء", "trash": "سلة المحذوفات", + "helpAndDocumentation": "المساعدة والتوثيق", + "getSupport": "احصل على الدعم", "helpAndSupport": "المساعدة والدعم" }, "sites": { diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index f388cf9bd5..8d98cb5cbc 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -133,14 +133,14 @@ "questionBubble": { "shortcuts": "Dreceres", "whatsNew": "Què hi ha de nou?", - "help": "Ajuda i Suport", "markdown": "Reducció", "debug": { "name": "Informació de depuració", "success": "S'ha copiat la informació de depuració!", "fail": "No es pot copiar la informació de depuració" }, - "feedback": "Feedback" + "feedback": "Feedback", + "help": "Ajuda i Suport" }, "menuAppHeader": { "moreButtonToolTip": "Suprimeix, canvia el nom i més...", diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 4acb7a1765..acfc571536 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -170,14 +170,14 @@ "questionBubble": { "shortcuts": "کورتە ڕێگاکان", "whatsNew": "نوێترین", - "help": "پشتیوانی و یارمەتی", "markdown": "Markdown", "debug": { "name": "زانیاری دیباگ", "success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!", "fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد" }, - "feedback": "فیدباک" + "feedback": "فیدباک", + "help": "پشتیوانی و یارمەتی" }, "menuAppHeader": { "moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...", diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 07e5a01bea..28750dd542 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -134,14 +134,14 @@ "questionBubble": { "shortcuts": "Klávesové zkratky", "whatsNew": "Co je nového?", - "help": "Pomoc a podpora", "markdown": "Markdown", "debug": { "name": "Debug informace", "success": "Debug informace zkopírovány do schránky!", "fail": "Nepodařilo se zkopáí" }, - "feedback": "Zpětná vazba" + "feedback": "Zpětná vazba", + "help": "Pomoc a podpora" }, "menuAppHeader": { "moreButtonToolTip": "Smazat, přejmenovat, a další...", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 60bb99b2fb..65a7fbea05 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -252,14 +252,14 @@ "questionBubble": { "shortcuts": "Tastenkürzel", "whatsNew": "Was gibt es Neues?", - "help": "Hilfe & Support", "markdown": "Markdown", "debug": { "name": "Debug-Informationen", "success": "Debug-Informationen in die Zwischenablage kopiert!", "fail": "Debug-Informationen konnten nicht in die Zwischenablage kopiert werden" }, - "feedback": "Feedback" + "feedback": "Feedback", + "help": "Hilfe & Support" }, "menuAppHeader": { "moreButtonToolTip": "Entfernen, umbenennen und mehr...", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index e73165349a..5f947ea015 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -218,14 +218,14 @@ "questionBubble": { "shortcuts": "Atajos", "whatsNew": "¿Qué hay de nuevo?", - "help": "Ayuda y Soporte", "markdown": "Reducción", "debug": { "name": "Información de depuración", "success": "¡Información copiada!", "fail": "No fue posible copiar la información" }, - "feedback": "Comentario" + "feedback": "Comentario", + "help": "Ayuda y Soporte" }, "menuAppHeader": { "moreButtonToolTip": "Eliminar, renombrar y más...", diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index be987e7a53..2e52231f7c 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -99,14 +99,14 @@ "questionBubble": { "shortcuts": "Lasterbideak", "whatsNew": "Ze berri?", - "help": "Laguntza", "markdown": "Markdown", "debug": { "name": "Debug informazioa", "success": "Debug informazioa kopiatu da!", "fail": "Ezin izan da debug informazioa kopiatu" }, - "feedback": "Iritzia" + "feedback": "Iritzia", + "help": "Laguntza" }, "menuAppHeader": { "addPageTooltip": "Gehitu orri bat", diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 80a100c3bc..cc93c17d64 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -139,14 +139,14 @@ "questionBubble": { "shortcuts": "میانبرها", "whatsNew": "تازه‌ترین‌ها", - "help": "پشتیبانی و مستندات", "markdown": "Markdown", "debug": { "name": "اطلاعات اشکال‌زدایی", "success": "طلاعات اشکال زدایی در کلیپ بورد کپی شد!", "fail": "نمی توان اطلاعات اشکال زدایی را در کلیپ بورد کپی کرد" }, - "feedback": "بازخورد" + "feedback": "بازخورد", + "help": "پشتیبانی و مستندات" }, "menuAppHeader": { "moreButtonToolTip": "حذف، تغییر نام، و موارد دیگر...", diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 7f8cdbd6a3..589d2dfe18 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -196,14 +196,14 @@ "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", - "help": "Aide et Support Technique", "markdown": "Réduction", "debug": { "name": "Infos du système", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour" + "feedback": "Retour", + "help": "Aide et Support Technique" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index f2c3e1270d..989e21f349 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -269,14 +269,14 @@ "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", - "help": "Aide et Support", "markdown": "Rédaction", "debug": { "name": "Informations de Débogage", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour" + "feedback": "Retour", + "help": "Aide et Support" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", diff --git a/frontend/resources/translations/he.json b/frontend/resources/translations/he.json index eda0cd920f..6c40f88947 100644 --- a/frontend/resources/translations/he.json +++ b/frontend/resources/translations/he.json @@ -206,14 +206,14 @@ "questionBubble": { "shortcuts": "מקשי קיצור", "whatsNew": "מה חדש?", - "help": "עזרה ותמיכה", "markdown": "Markdown", "debug": { "name": "פרטי ניפוי שגיאות", "success": "פרטי ניפוי השגיאות הועתקו ללוח הגזירים!", "fail": "לא ניתן להעתיק את פרטי ניפוי השגיאות ללוח הגזירים" }, - "feedback": "משוב" + "feedback": "משוב", + "help": "עזרה ותמיכה" }, "menuAppHeader": { "moreButtonToolTip": "הסרה, שינוי שם ועוד…", diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 40f05cccc6..1c10e40da4 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -103,14 +103,14 @@ "questionBubble": { "shortcuts": "Parancsikonok", "whatsNew": "Újdonságok", - "help": "Segítség & Támogatás", "markdown": "Markdown", "debug": { "name": "Debug Információ", "success": "Debug információ a vágólapra másolva", "fail": "A Debug információ nem másolható a vágólapra" }, - "feedback": "Visszacsatolás" + "feedback": "Visszacsatolás", + "help": "Segítség & Támogatás" }, "menuAppHeader": { "addPageTooltip": "Belső oldal hozzáadása", diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 74d1e69d63..b900929966 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -160,14 +160,14 @@ "questionBubble": { "shortcuts": "Pintasan", "whatsNew": "Apa yang baru?", - "help": "Bantuan & Dukungan", "markdown": "Penurunan harga", "debug": { "name": "Info debug", "success": "Info debug disalin ke papan klip!", "fail": "Tidak dapat menyalin info debug ke papan klip" }, - "feedback": "Masukan" + "feedback": "Masukan", + "help": "Bantuan & Dukungan" }, "menuAppHeader": { "moreButtonToolTip": "Menghapus, merubah nama, dan banyak lagi...", diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index d6877ecd59..7fb463da20 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -221,14 +221,14 @@ "questionBubble": { "shortcuts": "Scorciatoie", "whatsNew": "Cosa c'è di nuovo?", - "help": "Aiuto & Supporto", "markdown": "Markdown", "debug": { "name": "Informazioni di debug", "success": "Informazioni di debug copiate negli appunti!", "fail": "Impossibile copiare le informazioni di debug negli appunti" }, - "feedback": "Feedback" + "feedback": "Feedback", + "help": "Aiuto & Supporto" }, "menuAppHeader": { "moreButtonToolTip": "Rimuovi, rinomina e altro...", diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 15333ff3da..ebe679ad84 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -263,14 +263,14 @@ "questionBubble": { "shortcuts": "ショートカット", "whatsNew": "新着情報", - "help": "ヘルプ & サポート", "markdown": "Markdown", "debug": { "name": "デバッグ情報", "success": "デバッグ情報をクリップボードにコピーしました!", "fail": "デバッグ情報をクリップボードにコピーできませんでした" }, - "feedback": "フィードバック" + "feedback": "フィードバック", + "help": "ヘルプ & サポート" }, "menuAppHeader": { "moreButtonToolTip": "削除、名前の変更、その他...", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 0b2aabe508..1246b65f30 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -301,14 +301,14 @@ "questionBubble": { "shortcuts": "단축키", "whatsNew": "새로운 기능", - "help": "도움말 및 지원", "markdown": "Markdown", "debug": { "name": "디버그 정보", "success": "디버그 정보를 클립보드에 복사했습니다!", "fail": "디버그 정보를 클립보드에 복사할 수 없습니다" }, - "feedback": "피드백" + "feedback": "피드백", + "help": "도움말 및 지원" }, "menuAppHeader": { "moreButtonToolTip": "제거, 이름 변경 등...", diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index e3ca580354..9473d7e2f0 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -164,14 +164,14 @@ "questionBubble": { "shortcuts": "Skróty", "whatsNew": "Co nowego?", - "help": "Pomoc & Wsparcie", "markdown": "Markdown", "debug": { "name": "Informacje Debugowania", "success": "Skopiowano informacje debugowania do schowka!", "fail": "Nie mozna skopiować informacji debugowania do schowka" }, - "feedback": "Feedback" + "feedback": "Feedback", + "help": "Pomoc & Wsparcie" }, "menuAppHeader": { "moreButtonToolTip": "Usuń, zmień nazwę i więcej...", diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 51b585f14b..864d225095 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -225,14 +225,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", - "help": "Ajuda e Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Informação de depuração copiada para a área de transferência!", "fail": "Falha ao copiar a informação de depuração para a área de transferência" }, - "feedback": "Opinião" + "feedback": "Opinião", + "help": "Ajuda e Suporte" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 617e097f6b..c9892bf9df 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -128,14 +128,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", - "help": "Ajuda & Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Copiar informação de depuração para o clipboard!", "fail": "Falha em copiar a informação de depuração para o clipboard" }, - "feedback": "Opinião" + "feedback": "Opinião", + "help": "Ajuda & Suporte" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 2bac18a879..c45010b8fc 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -251,14 +251,14 @@ "questionBubble": { "shortcuts": "Горячие клавиши", "whatsNew": "Что нового?", - "help": "Помощь и поддержка", "markdown": "Markdown", "debug": { "name": "Отладочная информация", "success": "Отладочная информация скопирована в буфер обмена!", "fail": "Не удалось скопировать отладочную информацию в буфер обмена" }, - "feedback": "Обратная связь" + "feedback": "Обратная связь", + "help": "Помощь и поддержка" }, "menuAppHeader": { "moreButtonToolTip": "Удалить, переименовать и другие действия...", diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 42855011b2..3210aa1f15 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -107,14 +107,14 @@ "questionBubble": { "shortcuts": "Genvägar", "whatsNew": "Vad nytt?", - "help": "Hjälp & Support", "markdown": "Prissänkning", "debug": { "name": "Felsökningsinfo", "success": "Kopierade felsökningsinfo till urklipp!", "fail": "Kunde inte kopiera felsökningsinfo till urklipp" }, - "feedback": "Återkoppling" + "feedback": "Återkoppling", + "help": "Hjälp & Support" }, "menuAppHeader": { "addPageTooltip": "Lägg till en underliggande sida", diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 0be97f517e..78e5462d7f 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -250,14 +250,14 @@ "questionBubble": { "shortcuts": "ทางลัด", "whatsNew": "มีอะไรใหม่?", - "help": "ช่วยเหลือและสนับสนุน", "markdown": "Markdown", "debug": { "name": "ข้อมูลดีบัก", "success": "คัดลอกข้อมูลดีบักไปยังคลิปบอร์ดแล้ว!", "fail": "ไม่สามารถคัดลอกข้อมูลดีบักไปยังคลิปบอร์ด" }, - "feedback": "ข้อเสนอแนะ" + "feedback": "ข้อเสนอแนะ", + "help": "ช่วยเหลือและสนับสนุน" }, "menuAppHeader": { "moreButtonToolTip": "ลบ เปลี่ยนชื่อ และอื่นๆ...", diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 0e830b62c4..0eeac684c6 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -288,14 +288,14 @@ "questionBubble": { "shortcuts": "Kısayollar", "whatsNew": "Yenilikler", - "help": "Yardım ve Destek", "markdown": "Markdown", "debug": { "name": "Hata Ayıklama Bilgisi", "success": "Hata ayıklama bilgisi panoya kopyalandı!", "fail": "Hata ayıklama bilgisi panoya kopyalanamadı" }, - "feedback": "Geri Bildirim" + "feedback": "Geri Bildirim", + "help": "Yardım ve Destek" }, "menuAppHeader": { "moreButtonToolTip": "Kaldır, yeniden adlandır ve daha fazlası...", diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 3262de34a7..394801ed21 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -225,14 +225,14 @@ "questionBubble": { "shortcuts": "Комбінації клавіш", "whatsNew": "Що нового?", - "help": "Довідка та підтримка", "markdown": "Markdown", "debug": { "name": "Інформація для налагодження", "success": "Інформацію для налагодження скопійовано в буфер обміну!", "fail": "Не вдалося скопіювати інформацію для налагодження в буфер обміну" }, - "feedback": "Зворотний зв'язок" + "feedback": "Зворотний зв'язок", + "help": "Довідка та підтримка" }, "menuAppHeader": { "moreButtonToolTip": "Видалити, перейменувати та інше...", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 957fdd6e03..e60648590d 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -226,14 +226,14 @@ "questionBubble": { "shortcuts": "Phím tắt", "whatsNew": "Có gì mới?", - "help": "Trợ giúp & Hỗ trợ", "markdown": "Markdown", "debug": { "name": "Thông tin gỡ lỗi", "success": "Đã sao chép thông tin gỡ lỗi vào khay nhớ tạm!", "fail": "Không thể sao chép thông tin gỡ lỗi vào khay nhớ tạm" }, - "feedback": "Nhận xét" + "feedback": "Nhận xét", + "help": "Trợ giúp & Hỗ trợ" }, "menuAppHeader": { "moreButtonToolTip": "Xóa, đổi tên và hơn thế nữa...", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 44c46d8b5c..df65cbac4b 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -270,14 +270,14 @@ "questionBubble": { "shortcuts": "快捷键", "whatsNew": "新功能", - "help": "帮助和支持", "markdown": "Markdown", "debug": { "name": "调试信息", "success": "将调试信息复制到剪贴板!", "fail": "无法将调试信息复制到剪贴板" }, - "feedback": "反馈" + "feedback": "反馈", + "help": "帮助和支持" }, "menuAppHeader": { "moreButtonToolTip": "删除、重命名等等...", diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index acd121049e..b5f4ff3d5f 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -216,14 +216,14 @@ "questionBubble": { "shortcuts": "快捷鍵", "whatsNew": "有什麼新功能?", - "help": "幫助 & 支援", "markdown": "Markdown", "debug": { "name": "除錯資訊", "success": "已將除錯資訊複製到剪貼簿!", "fail": "無法將除錯資訊複製到剪貼簿" }, - "feedback": "意見回饋" + "feedback": "意見回饋", + "help": "幫助 & 支援" }, "menuAppHeader": { "moreButtonToolTip": "移除、重新命名等等...", From 7d0f6c9debd8dee526cf846e4ba276bd7361dc22 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:38:42 +0800 Subject: [PATCH 287/384] fix(mobile): edit mention page (#7698) * fix(mobile): edit mention page * chore: type check * chore: replace usages * chore: use MentionType instead --- .../base/insert_page_command.dart | 13 +++-- .../copy_and_paste/paste_from_block_link.dart | 12 ++--- .../child_page_transaction_handler.dart | 13 ++--- .../mention/date_transaction_handler.dart | 15 +++--- .../editor_plugins/mention/mention_block.dart | 47 ++++++++++++++++--- .../mention/mention_date_block.dart | 21 +++++---- .../mention/mention_page_block.dart | 37 +++++++-------- .../slash_menu_items/date_item.dart | 12 ++--- .../inline_actions/handlers/child_page.dart | 11 ++--- .../handlers/date_reference.dart | 12 ++--- .../handlers/inline_page_reference.dart | 11 ++--- .../handlers/reminder_reference.dart | 14 +++--- 12 files changed, 118 insertions(+), 100 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index af605972de..11aed036d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -70,13 +70,12 @@ extension InsertDatabase on EditorState { node, selection.end.offset, 0, - r'$', - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.pageId: view.id, - }, - }, + MentionBlockKeys.mentionChar, + attributes: MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: view.id, + blockId: null, + ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart index fbd9914c1d..c47c0c967d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart @@ -43,13 +43,11 @@ extension PasteFromBlockLink on EditorState { node, selection.startIndex, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.blockId: blockId, - MentionBlockKeys.pageId: pageId, - }, - }, + attributes: MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: pageId, + blockId: blockId, + ), ); await apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart index ef32ad1098..117e09e67f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart @@ -179,13 +179,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { await duplicatedViewOrFailure.fold( (newView) async { - final newMentionAttributes = { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.childPage.name, - MentionBlockKeys.pageId: newView.id, - }, - }; - // The index is the index of the delta, to get the index of the mention character // in all the text, we need to calculate it based on the deltas before the current delta. int mentionIndex = 0; @@ -202,7 +195,11 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { node, mentionIndex, MentionBlockKeys.mentionChar.length, - newMentionAttributes, + MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.childPage, + pageId: newView.id, + blockId: null, + ), ); await editorState.apply( transaction, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart index 972ed229dd..cb3196e9b7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart @@ -192,15 +192,12 @@ class DateTransactionHandler extends MentionTransactionHandler { ), ); - final newMentionAttributes = { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: dateTime.toIso8601String(), - MentionBlockKeys.reminderId: reminderId, - MentionBlockKeys.includeTime: data.includeTime, - MentionBlockKeys.reminderOption: data.reminderOption.name, - }, - }; + final newMentionAttributes = MentionBlockKeys.buildMentionDateAttributes( + date: dateTime.toIso8601String(), + reminderId: reminderId, + reminderOption: data.reminderOption.name, + includeTime: data.includeTime, + ); // The index is the index of the delta, to get the index of the mention character // in all the text, we need to calculate it based on the deltas before the current delta. diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index d65609f1a8..79c9b31d20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -27,12 +27,12 @@ Node dateMentionNode() { operations: [ TextInsert( MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: DateTime.now().toIso8601String(), - }, - }, + attributes: MentionBlockKeys.buildMentionDateAttributes( + date: DateTime.now().toIso8601String(), + reminderId: null, + reminderOption: null, + includeTime: false, + ), ), ], ), @@ -42,18 +42,51 @@ Node dateMentionNode() { class MentionBlockKeys { const MentionBlockKeys._(); - static const reminderId = 'reminder_id'; // ReminderID static const mention = 'mention'; static const type = 'type'; // MentionType, String + static const pageId = 'page_id'; static const blockId = 'block_id'; // Related to Reminder and Date blocks static const date = 'date'; // Start Date static const includeTime = 'include_time'; + static const reminderId = 'reminder_id'; // ReminderID static const reminderOption = 'reminder_option'; static const mentionChar = '\$'; + + static Map buildMentionPageAttributes({ + required MentionType mentionType, + required String pageId, + required String? blockId, + }) { + return { + MentionBlockKeys.mention: { + MentionBlockKeys.type: mentionType.name, + MentionBlockKeys.pageId: pageId, + if (blockId != null) MentionBlockKeys.blockId: blockId, + }, + }; + } + + static Map buildMentionDateAttributes({ + required String date, + required String? reminderId, + required String? reminderOption, + required bool includeTime, + }) { + return { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date, + MentionBlockKeys.includeTime: includeTime, + if (reminderId != null) MentionBlockKeys.reminderId: reminderId, + if (reminderOption != null) + MentionBlockKeys.reminderOption: reminderOption, + }, + }; + } } class MentionBlock extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 6c5ee7f527..20f60be23d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -201,16 +201,17 @@ class _MentionDateBlockState extends State { (reminderOption == ReminderOption.none ? null : widget.reminderId); final transaction = widget.editorState.transaction - ..formatText(widget.node, widget.index, 1, { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: date.toIso8601String(), - MentionBlockKeys.reminderId: rId, - MentionBlockKeys.includeTime: includeTime, - MentionBlockKeys.reminderOption: - reminderOption?.name ?? widget.reminderOption.name, - }, - }); + ..formatText( + widget.node, + widget.index, + 1, + MentionBlockKeys.buildMentionDateAttributes( + date: date.toIso8601String(), + reminderId: rId, + includeTime: includeTime, + reminderOption: reminderOption?.name ?? widget.reminderOption.name, + ), + ); widget.editorState.apply(transaction, withUpdateSelection: false); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index 23c05b89eb..ede690eb30 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -50,12 +50,11 @@ Node pageMentionNode(String viewId) { operations: [ TextInsert( MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.pageId: viewId, - }, - }, + attributes: MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: viewId, + blockId: null, + ), ), ], ), @@ -284,12 +283,11 @@ class _MentionSubPageBlockState extends State { widget.node, widget.index, MentionBlockKeys.mentionChar.length, - { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.pageId: widget.pageId, - }, - }, + MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: widget.pageId, + blockId: null, + ), ); widget.editorState.apply( @@ -383,25 +381,24 @@ Future _handleDoubleTap( } final currentViewId = context.read().documentId; - final newViewId = await showPageSelectorSheet( + final newView = await showPageSelectorSheet( context, currentViewId: currentViewId, selectedViewId: viewId, ); - if (newViewId != null) { + if (newView != null) { // Update this nodes pageId final transaction = editorState.transaction ..formatText( node, index, 1, - { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.pageId: newViewId, - }, - }, + MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: newView.id, + blockId: null, + ), ); await editorState.apply(transaction, withUpdateSelection: false); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart index 0bb09d0a7e..04a8ee1b7e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart @@ -46,12 +46,12 @@ extension on EditorState { selection.start.offset, 0, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: DateTime.now().toIso8601String(), - }, - }, + attributes: MentionBlockKeys.buildMentionDateAttributes( + date: DateTime.now().toIso8601String(), + reminderId: null, + reminderOption: null, + includeTime: false, + ), ); await apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart index d29a1f86bf..665c284aea 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart @@ -71,12 +71,11 @@ class InlineChildPageService extends InlineActionsDelegate { replacement.$1, replacement.$2, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.childPage.name, - MentionBlockKeys.pageId: view.id, - }, - }, + attributes: MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.childPage, + pageId: view.id, + blockId: null, + ), ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index c7076bd255..747c8667f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -122,12 +122,12 @@ class DateReferenceService extends InlineActionsDelegate { start, end, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: date.toIso8601String(), - }, - }, + attributes: MentionBlockKeys.buildMentionDateAttributes( + date: date.toIso8601String(), + includeTime: false, + reminderId: null, + reminderOption: null, + ), ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 27a632e8ef..ebc684327b 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -221,12 +221,11 @@ class InlinePageReferenceService extends InlineActionsDelegate { replace.$1, replace.$2, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.page.name, - MentionBlockKeys.pageId: view.id, - }, - }, + attributes: MentionBlockKeys.buildMentionPageAttributes( + mentionType: MentionType.page, + pageId: view.id, + blockId: null, + ), ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index 1f479fa7c5..471f1c9211 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -148,14 +148,12 @@ class ReminderReferenceService extends InlineActionsDelegate { start, end, MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: date.toIso8601String(), - MentionBlockKeys.reminderId: reminder.id, - MentionBlockKeys.reminderOption: ReminderOption.atTimeOfEvent.name, - }, - }, + attributes: MentionBlockKeys.buildMentionDateAttributes( + date: date.toIso8601String(), + reminderId: reminder.id, + reminderOption: ReminderOption.atTimeOfEvent.name, + includeTime: false, + ), ); await editorState.apply(transaction); From 13028c6cfe3b105221ec4a88a22ad91c906ca64e Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 9 Apr 2025 09:39:35 +0800 Subject: [PATCH 288/384] fix: icon and emoji doesn't align in mention page menu (#7673) --- .../mobile_inline_actions_menu_group.dart | 4 ++-- .../header/emoji_icon_widget.dart | 16 +++++++--------- .../inline_actions/handlers/child_page.dart | 2 +- .../handlers/inline_page_reference.dart | 19 +++++++++++++------ .../inline_actions/inline_actions_result.dart | 4 ++-- .../widgets/inline_actions_menu_group.dart | 6 +++--- .../home/menu/view/view_item.dart | 6 +++++- 7 files changed, 33 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart index 862c9876f2..f340319254 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart @@ -102,7 +102,7 @@ class MobileInlineActionsWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final hasIcon = item.icon != null; + final hasIcon = item.iconBuilder != null; return Container( height: 36, decoration: BoxDecoration( @@ -119,7 +119,7 @@ class MobileInlineActionsWidget extends StatelessWidget { child: Row( children: [ if (hasIcon) ...[ - item.icon!.call(isSelected), + item.iconBuilder!.call(isSelected), SizedBox(width: 12), ], Flexible( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 91ae1af354..cda76233d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -8,6 +8,7 @@ import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; import 'package:string_validator/string_validator.dart'; @@ -113,24 +114,21 @@ class _RawEmojiIconWidgetState extends State { try { switch (widget.emoji.type) { case FlowyIconType.emoji: - return EmojiText( - emoji: widget.emoji.emoji, + return FlowyText.emoji( + widget.emoji.emoji, fontSize: widget.emojiSize, textAlign: TextAlign.justify, lineHeight: widget.lineHeight, ); case FlowyIconType.icon: - IconsData iconData = - IconsData.fromJson(jsonDecode(widget.emoji.emoji)); + IconsData iconData = IconsData.fromJson( + jsonDecode(widget.emoji.emoji), + ); if (!widget.enableColor) { iconData = iconData.noColor(); } - /// Under the same width conditions, icons on macOS seem to appear - /// larger than emojis, so 0.9 is used here to slightly reduce the - /// size of the icons - final iconSize = - Platform.isMacOS ? widget.emojiSize * 0.9 : widget.emojiSize; + final iconSize = widget.emojiSize; return IconWidget( iconsData: iconData, size: iconSize, diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart index 665c284aea..6dbd38affb 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart @@ -24,7 +24,7 @@ class InlineChildPageService extends InlineActionsDelegate { results.add( InlineActionsMenuItem( label: LocaleKeys.inlineActions_createPage.tr(args: [search]), - icon: (_) => const FlowySvg(FlowySvgs.add_s), + iconBuilder: (_) => const FlowySvg(FlowySvgs.add_s), onSelected: (context, editorState, service, replacement) => _onSelected(context, editorState, service, replacement, search), ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index ebc684327b..9853d6757c 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -234,12 +234,19 @@ class InlinePageReferenceService extends InlineActionsDelegate { InlineActionsMenuItem _fromView(ViewPB view) => InlineActionsMenuItem( keywords: [view.nameOrDefault.toLowerCase()], label: view.nameOrDefault, - icon: (onSelected) => view.icon.value.isNotEmpty - ? RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 14, - ) - : view.defaultIcon(), + iconBuilder: (onSelected) { + final child = view.icon.value.isNotEmpty + ? RawEmojiIconWidget( + emoji: view.icon.toEmojiIconData(), + emojiSize: 16.0, + lineHeight: 18.0 / 16.0, + ) + : view.defaultIcon(size: const Size(16, 16)); + return SizedBox( + width: 16, + child: child, + ); + }, onSelected: (context, editorState, menu, replace) => insertPage ? _onInsertPageRef(view, context, editorState, replace) : _onInsertLinkRef(view, context, editorState, menu, replace), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart index 8da9647084..1fe2703870 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart @@ -12,13 +12,13 @@ typedef SelectItemHandler = void Function( class InlineActionsMenuItem { InlineActionsMenuItem({ required this.label, - this.icon, + this.iconBuilder, this.keywords, this.onSelected, }); final String label; - final Widget Function(bool onSelected)? icon; + final Widget Function(bool onSelected)? iconBuilder; final List? keywords; final SelectItemHandler? onSelected; } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart index f2d22138f7..123cfc1177 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart @@ -92,8 +92,8 @@ class InlineActionsWidget extends StatefulWidget { class _InlineActionsWidgetState extends State { @override Widget build(BuildContext context) { - final icon = widget.item.icon; - final hasIcon = icon != null; + final iconBuilder = widget.item.iconBuilder; + final hasIcon = iconBuilder != null; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( @@ -104,7 +104,7 @@ class _InlineActionsWidgetState extends State { text: Row( children: [ if (hasIcon) ...[ - icon.call(widget.isSelected), + iconBuilder.call(widget.isSelected), SizedBox(width: 12), ], Flexible( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index ae9059c623..22182f7429 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -632,7 +632,11 @@ class _SingleInnerViewItemState extends State { Widget _buildViewIconButton() { final iconData = widget.view.icon.toEmojiIconData(); final icon = iconData.isNotEmpty - ? RawEmojiIconWidget(emoji: iconData, emojiSize: 16.0) + ? RawEmojiIconWidget( + emoji: iconData, + emojiSize: 16.0, + lineHeight: 18.0 / 16.0, + ) : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); final Widget child = AppFlowyPopover( From b3a4a9ba0452433578abc3c8d6d326805eefcda5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:25:29 +0800 Subject: [PATCH 289/384] fix: use normal underline for AI suggested text style (#7712) --- .../plugins/document/presentation/editor_style.dart | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index c602ade2c4..bd205ffea9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -568,7 +568,6 @@ class EditorStyleCustomizer { if (style == null) { return null; } - final fontSize = style.fontSize ?? 14.0; final isLight = Theme.of(context).isLightMode; final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4); final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4); @@ -578,17 +577,10 @@ class EditorStyleCustomizer { decoration: TextDecoration.lineThrough, ), AiWriterBlockKeys.suggestionReplacement => style.copyWith( - color: Colors.transparent, + color: textColor, decoration: TextDecoration.underline, decorationColor: underlineColor, decorationThickness: 1.0, - // hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb - shadows: [ - Shadow( - color: textColor, - offset: Offset(0, -fontSize * 0.2), - ), - ], ), _ => style, }; From f4af47728fb59241a8c490db64f5f0be6b479b4f Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 9 Apr 2025 12:57:58 +0800 Subject: [PATCH 290/384] fix: overwriting a selected text removes more than expected (#7714) --- .../document/document_selection_test.dart | 37 +++++++++++++++++++ .../plugins/emoji/emoji_actions_command.dart | 6 +-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart index bd0fd18c50..de1cb880a5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.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'; @@ -47,5 +48,41 @@ void main() { expect(editorState.selection!.start.offset, 0); }); + + testWidgets('select and delete text', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// create a new document + await tester.createNewPageWithNameUnderParent(); + + /// input text + final editor = tester.editor; + final editorState = editor.getCurrentEditorState(); + + const inputText = 'Test for text selection and deletion'; + final texts = inputText.split(' '); + await editor.tapLineOfEditorAt(0); + await tester.ime.insertText(inputText); + + /// selecte and delete + int index = 0; + while (texts.isNotEmpty) { + final text = texts.removeAt(0); + await tester.editor.updateSelection( + Selection( + start: Position(path: [0], offset: index), + end: Position(path: [0], offset: index + text.length), + ), + ); + await tester.simulateKeyEvent(LogicalKeyboardKey.delete); + index++; + } + + /// excpete the text value is correct + final node = editorState.getNodeAtPath([0])!; + final nodeText = node.delta?.toPlainText() ?? ''; + expect(nodeText, ' ' * (index - 1)); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart index ffe25be8bd..9d386b36be 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart @@ -38,10 +38,6 @@ Future emojiCommandHandler( return false; } - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - final node = editorState.getNodeAtPath(selection.end.path); final delta = node?.delta; if (node == null || @@ -58,6 +54,8 @@ Future emojiCommandHandler( if (previousCharacter != _emojiCharacter) return false; if (!context.mounted) return false; + if (!selection.isCollapsed) return false; + await editorState.insertTextAtPosition( character, position: selection.start, From 8b82d532e019efa7a29261b6cd1f93846dd1e675 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 9 Apr 2025 13:21:19 +0800 Subject: [PATCH 291/384] fix: replace the selected text with ai response in the same line (#7708) * fix: replace the selected text with ai response in the same line * fix: replace the selected text with ai response in the multiple lines * fix: integrate the replace function in the ai writer cubit * fix: unit test and integration test --- .../ai/operations/ai_writer_cubit.dart | 62 ++- .../operations/ai_writer_node_extension.dart | 21 +- .../base/markdown_text_robot.dart | 188 ++++++- .../lib/shared/markdown_to_document.dart | 2 + frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../ai_writer_test/ai_writer_bloc_test.dart | 2 +- .../text_robot/markdown_text_robot_test.dart | 508 ++++++++++++++++++ 8 files changed, 773 insertions(+), 16 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 076046624a..7a885a3fe8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -15,6 +16,11 @@ import 'ai_writer_block_operations.dart'; import 'ai_writer_entities.dart'; import 'ai_writer_node_extension.dart'; +/// Enable the debug log for the AiWriterCubit. +/// +/// This is useful for debugging the AI writer cubit. +const _aiWriterCubitDebugLog = false; + class AiWriterCubit extends Cubit { AiWriterCubit({ required this.documentId, @@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit { final command = node.aiWriterCommand; final (run, prompt) = await _addSelectionTextToRecords(command); + _aiWriterCubitLog( + 'command: $command, run: $run, prompt: $prompt', + ); + if (!run) { await exit(); return; @@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit { return; } + // Accept + // + // If the user clicks accept, we need to replace the selection with the AI's response if (action case SuggestionAction.accept) { - await _textRobot.persist(); - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, + // trim the markdown text to avoid extra new lines + final trimmedMarkdownText = _textRobot.markdownText.trim(); + + _aiWriterCubitLog( + 'trigger accept action, markdown text: $trimmedMarkdownText', ); - final nodes = editorState.getNodesInSelection(selection); - final transaction = editorState.transaction..deleteNodes(nodes); - await editorState.apply( - transaction, - withUpdateSelection: false, + + await _textRobot.deleteAINodes(); + + await _textRobot.replace( + selection: selection, + markdownText: trimmedMarkdownText, ); + await exit(withDiscard: false, withUnformat: false); + return; } @@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit { AiWriterCommand command, ) async { final node = aiWriterNode; + + // check the node is registered if (node == null) { return (false, ''); } + + // check the selection is valid final selection = node.aiWriterSelection?.normalized; if (selection == null) { return (false, ''); } + // if the command is continue writing, we don't need to get the selection text if (command == AiWriterCommand.continueWriting) { return (true, ''); } + + // if the selection is collapsed, we don't need to get the selection text if (selection.isCollapsed) { return (true, ''); } @@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit { records.add( AiWriterRecord.user(content: selectionText, format: null), ); + return (true, ''); } else { return (true, selectionText); @@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit { attributes: ApplySuggestionFormatType.replace.attributes, ); onAppendToDocument?.call(); + + _aiWriterCubitLog( + 'received message: $text', + ); }, processAssistMessage: (text) async { if (state case final GeneratingAiWriterState generatingState) { @@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit { ), ); } + + _aiWriterCubitLog( + 'received assist message: $text', + ); }, onEnd: () async { if (state case final GeneratingAiWriterState generatingState) { @@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit { records.add( AiWriterRecord.ai(content: _textRobot.markdownText), ); + + _aiWriterCubitLog( + 'returned response: ${_textRobot.markdownText}', + ); } }, onError: (error) async { @@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit { ); } } + + void _aiWriterCubitLog(String message) { + if (_aiWriterCubitDebugLog) { + Log.debug('[AiWriterCubit] $message'); + } + } } mixin RegisteredAiWriter { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 168854a34d..6f4456f9ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState { slicedNodes.add(copiedNode); } + for (final (i, node) in slicedNodes.indexed) { + final childNodesShouldBeDeleted = []; + for (final child in node.children) { + if (!child.path.inSelection(selection)) { + childNodesShouldBeDeleted.add(child); + } + } + for (final child in childNodesShouldBeDeleted) { + slicedNodes[i] = node.copyWith( + children: node.children.where((e) => e.id != child.id).toList(), + type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type, + ); + } + } + + // use \n\n as line break to improve the ai response + // using \n will cause the ai response treat the text as a single line final markdown = await customDocumentToMarkdown( Document.blank()..insert([0], slicedNodes), + lineBreak: '\n\n', ); - return markdown; + // trim the last \n if it exists + return markdown.trimRight(); } List getPlainTextInSelection(Selection? selection) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index d6cfee929c..031e157b33 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -110,10 +110,13 @@ class MarkdownTextRobot { } /// Persist the text into the document - Future persist({String? markdownText}) async { + Future persist({ + String? markdownText, + }) async { if (markdownText != null) { _markdownText = markdownText; } + await _lock.synchronized(() async { await _refresh(inMemoryUpdate: false); }); @@ -124,6 +127,34 @@ class MarkdownTextRobot { } } + /// Replace the selected content with the AI's response + Future replace({ + required Selection selection, + required String markdownText, + }) async { + if (selection.isSingle) { + await _replaceInSameLine( + selection: selection, + markdownText: markdownText, + ); + } else { + await _replaceInMultiLines( + selection: selection, + markdownText: markdownText, + ); + } + } + + /// Delete the temporary inserted AI nodes + Future deleteAINodes() async { + final nodes = getInsertedNodes(); + final transaction = editorState.transaction..deleteNodes(nodes); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + ); + } + /// Discard the inserted content Future discard() async { final start = _insertPosition; @@ -282,6 +313,161 @@ class MarkdownTextRobot { children: children, ); } + + /// If the selected content is in the same line, + /// keep the selected node and replace the delta. + Future _replaceInSameLine({ + required Selection selection, + required String markdownText, + }) async { + selection = selection.normalized; + + // If the selection is not a single node, do nothing. + if (!selection.isSingle) { + assert(false, 'Expected single node selection'); + Log.error('Expected single node selection'); + return; + } + + final startIndex = selection.startIndex; + final endIndex = selection.endIndex; + final length = endIndex - startIndex; + + // Get the selected node. + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + assert(false, 'Expected non-null node and delta'); + Log.error('Expected non-null node and delta'); + return; + } + + // Convert the markdown text to delta. + // Question: Why we need to convert the markdown to document first? + // Answer: Because the markdown text may contain the list item, + // if we convert the markdown to delta directly, the list item will be + // treated as a normal text node, and the delta will be incorrect. + // For example, the markdown text is: + // ``` + // 1. item1 + // ``` + // if we convert the markdown to delta directly, the delta will be: + // ``` + // [ + // { + // "insert": "1. item1" + // } + // ] + // ``` + // if we convert the markdown to document first, the document will be: + // ``` + // [ + // { + // "type": "numbered_list", + // "children": [ + // { + // "insert": "item1" + // } + // ] + // } + // ] + final document = customMarkdownToDocument(markdownText); + final decoder = DeltaMarkdownDecoder(); + final markdownDelta = + document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText); + + // Replace the delta of the selected node. + final transaction = editorState.transaction; + transaction + ..deleteText(node, startIndex, length) + ..insertTextDelta(node, startIndex, markdownDelta); + await editorState.apply(transaction); + } + + /// If the selected content is in multiple lines + Future _replaceInMultiLines({ + required Selection selection, + required String markdownText, + }) async { + selection = selection.normalized; + + // If the selection is a single node, do nothing. + if (selection.isSingle) { + assert(false, 'Expected multi-line selection'); + Log.error('Expected multi-line selection'); + return; + } + + final markdownNodes = customMarkdownToDocument( + markdownText, + tableWidth: 250.0, + ).root.children; + + // Get the selected nodes. + final nodes = editorState.getNodesInSelection(selection); + + // Note: Don't change its order, otherwise the delta will be incorrect. + // step 1. merge the first selected node and the first node from the ai response + // step 2. merge the last selected node and the last node from the ai response + // step 3. insert the middle nodes from the ai response + // step 4. delete the middle nodes + final transaction = editorState.transaction; + + // step 1 + final firstNode = nodes.firstOrNull; + final delta = firstNode?.delta; + final firstMarkdownNode = markdownNodes.firstOrNull; + final firstMarkdownDelta = firstMarkdownNode?.delta; + if (firstNode != null && + delta != null && + firstMarkdownNode != null && + firstMarkdownDelta != null) { + final startIndex = selection.startIndex; + final length = delta.length - startIndex; + + transaction + ..deleteText(firstNode, startIndex, length) + ..insertTextDelta(firstNode, startIndex, firstMarkdownDelta); + } + + // step 2 + final lastNode = nodes.lastOrNull; + final lastDelta = lastNode?.delta; + final lastMarkdownNode = markdownNodes.lastOrNull; + final lastMarkdownDelta = lastMarkdownNode?.delta; + if (lastNode != null && + lastDelta != null && + lastMarkdownNode != null && + lastMarkdownDelta != null) { + final endIndex = selection.endIndex; + + transaction.deleteText(lastNode, 0, endIndex); + + // if the last node is same as the first node, it means we have replaced the + // selected text in the first node. + if (lastMarkdownNode.id != firstMarkdownNode?.id) { + transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta); + } + } + + // step 3 + final insertedPath = selection.start.path.nextNPath(1); + if (markdownNodes.length > 2) { + transaction.insertNodes( + insertedPath, + markdownNodes.skip(1).take(markdownNodes.length - 2).toList(), + ); + } + + // step 4 + final length = nodes.length - 2; + if (length > 0) { + final middleNodes = nodes.skip(1).take(length).toList(); + transaction.deleteNodes(middleNodes); + } + + await editorState.apply(transaction); + } } class AINodeExternalValues extends NodeExternalValues { diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 4be6fdbe11..912f96bd05 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -27,6 +27,7 @@ Future customDocumentToMarkdown( Document document, { String path = '', AsyncValueSetter? onArchive, + String lineBreak = '', }) async { final List> fileFutures = []; @@ -41,6 +42,7 @@ Future customDocumentToMarkdown( try { markdown = documentToMarkdown( document, + lineBreak: lineBreak, customParsers: [ const MathEquationNodeParser(), const CalloutNodeParser(), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 083f945636..fecbc32b89 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "552f95f" - resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25" + ref: "361b99c38370abeeb19656f89e8c31cb3666623b" + resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e6e377fa24..0dc9ec673c 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -184,7 +184,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "552f95f" + ref: "361b99c38370abeeb19656f89e8c31cb3666623b" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index bcd8b13d39..46b8118087 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -375,7 +375,7 @@ void main() { await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); - expect(editorState.document.root.children.length, 1); + expect(editorState.document.root.children.length, 2); expect( editorState.getNodeAtPath([0])!.delta!.toPlainText(), 'Hello World', diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart index 0b6289c784..f2fcf8cd65 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart @@ -294,6 +294,514 @@ void main() { ); }); }); + + group('markdown text robot - replace in same line:', () { + final text1 = + '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; + final text2 = + '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; + final text3 = + '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; + + Document buildTestDocument() { + return Document( + root: pageNode( + children: [ + paragraphNode(delta: Delta()..insert(text1 + text2 + text3)), + ], + ), + ); + } + + // 1. create a document with a paragraph node + // 2. use the text robot to replace the selected content in the same line + // 3. check the document + test('the selection is in the middle of the text', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: text1.length, + ), + end: Position( + path: [0], + offset: text1.length + text2.length, + ), + ); + + final markdownText = + '''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 5); + + final d1 = afterDelta[0] as TextInsert; + expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the '); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'World Wide Web'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect(d3.text, ' transformed the internet, making it accessible to '); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'non-technical users'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect( + d5.text, + ' and opening the floodgates for global mass adoption.$text3', + ); + expect(d5.attributes, null); + }); + + test('replace markdown text with selection from start to middle', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + ), + end: Position( + path: [0], + offset: text1.length, + ), + ); + + final markdownText = + '''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 5); + + final d1 = afterDelta[0] as TextInsert; + expect(d1.text, 'The '); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'invention'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect(d3.text, ' of the '); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'World Wide Web'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect( + d5.text, + ' by Tim Berners-Lee transformed how we access information.$text2$text3', + ); + expect(d5.attributes, null); + }); + + test('replace markdown text with selection from middle to end', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: text1.length + text2.length, + ), + end: Position( + path: [0], + offset: text1.length + text2.length + text3.length, + ), + ); + + final markdownText = + '''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 7); + + final d1 = afterDelta[0] as TextInsert; + expect( + d1.text, + text1 + text2, + ); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'Email'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect( + d3.text, + ' became widespread, and instant messaging services like ', + ); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'ICQ'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect(d5.text, ' and '); + expect(d5.attributes, null); + + final d6 = afterDelta[5] as TextInsert; + expect( + d6.text, + 'AOL Instant Messenger', + ); + expect(d6.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d7 = afterDelta[6] as TextInsert; + expect( + d7.text, + ' gained tremendous popularity, allowing for seamless real-time text communication across the globe.', + ); + expect(d7.attributes, null); + }); + }); + + group('markdown text robot - replace in multiple lines:', () { + final text1 = + '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; + final text2 = + '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; + final text3 = + '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; + + Document buildTestDocument() { + return Document( + root: pageNode( + children: [ + paragraphNode(delta: Delta()..insert(text1)), + paragraphNode(delta: Delta()..insert(text2)), + paragraphNode(delta: Delta()..insert(text3)), + ], + ), + ); + } + + // 1. create a document with 3 paragraph nodes + // 2. use the text robot to replace the selected content in the multiple lines + // 3. check the document + test( + 'the selection starts with the first paragraph and ends with the middle of second paragraph', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + ), + end: Position( + path: [1], + offset: text2.length - + ', opening the floodgates for mass adoption. '.length, + ), + ); + + final markdownText = + '''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point. + +Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 3); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, 'The '); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, 'introduction'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, ' of the World Wide Web in the '); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, 'early 1990s'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, ' marked a significant turning point.'); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 3); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, "Tim Berners-Lee's "); + expect(d1.attributes, null); + + final d2 = delta2[1] as TextInsert; + expect(d2.text, "revolutionary invention"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta2[2] as TextInsert; + expect( + d3.text, + " made the internet accessible to non-technical users, opening the floodgates for mass adoption. ", + ); + expect(d3.attributes, null); + } + + { + // third paragraph + final delta3 = afterNodes[2].delta!.toList(); + expect(delta3.length, 1); + + final d1 = delta3[0] as TextInsert; + expect(d1.text, text3); + expect(d1.attributes, null); + } + }); + + test( + 'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: 'The introduction of the World Wide Web'.length, + ), + end: Position( + path: [2], + offset: + 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' + .length, + ), + ); + + final markdownText = + ''' in the **early 1990s** marked a *significant turning point* in technological history. + +Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*. + +Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity + '''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 3); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, 'The introduction of the World Wide Web in the '); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, 'early 1990s'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, ' marked a '); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, 'significant turning point'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, ' in technological history.'); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 5); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, "Tim Berners-Lee's "); + expect(d1.attributes, null); + + final d2 = delta2[1] as TextInsert; + expect(d2.text, "revolutionary invention"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta2[2] as TextInsert; + expect( + d3.text, + " made the internet accessible to non-technical users, opening the floodgates for ", + ); + expect(d3.attributes, null); + + final d4 = delta2[3] as TextInsert; + expect(d4.text, "unprecedented mass adoption"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta2[4] as TextInsert; + expect(d5.text, "."); + expect(d5.attributes, null); + } + + { + // third paragraph + // third paragraph + final delta3 = afterNodes[2].delta!.toList(); + expect(delta3.length, 7); + + final d1 = delta3[0] as TextInsert; + expect(d1.text, "Email became "); + expect(d1.attributes, null); + + final d2 = delta3[1] as TextInsert; + expect(d2.text, "widely prevalent"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta3[2] as TextInsert; + expect(d3.text, ", and instant messaging services like "); + expect(d3.attributes, null); + + final d4 = delta3[3] as TextInsert; + expect(d4.text, "ICQ"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta3[4] as TextInsert; + expect(d5.text, " and "); + expect(d5.attributes, null); + + final d6 = delta3[5] as TextInsert; + expect(d6.text, "AOL Instant Messenger"); + expect(d6.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d7 = delta3[6] as TextInsert; + expect( + d7.text, + " gained tremendous popularity, allowing for real-time text communication.", + ); + expect(d7.attributes, null); + } + }); + + test( + 'the length of the returned response less than the length of the selected text', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: 'The introduction of the World Wide Web'.length, + ), + end: Position( + path: [2], + offset: + 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' + .length, + ), + ); + + final markdownText = + ''' in the **early 1990s** marked a *significant turning point* in technological history.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 2); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, "The introduction of the World Wide Web in the "); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, "early 1990s"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, " marked a "); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, "significant turning point"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, " in technological history."); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 1); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, ", allowing for real-time text communication."); + expect(d1.attributes, null); + } + }); + }); } const _sample1 = '''# The Curious Cat From ad227bcf79b973fcc994d3d4c0fa507e823dd088 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:45:07 +0800 Subject: [PATCH 292/384] chore: toast message when theme folder doesn't have permission (#7715) --- .../lib/core/helpers/url_launcher.dart | 1 - .../presentation/base/mobile_view_page.dart | 2 -- .../base/view_page/more_bottom_sheet.dart | 11 ----------- .../bottom_sheet/bottom_sheet_view_item.dart | 3 --- .../default_mobile_action_pane.dart | 2 -- .../card_detail/widgets/row_page_button.dart | 2 +- .../presentation/home/mobile_home_page.dart | 2 +- .../home/space/mobile_space_menu.dart | 4 ---- .../mobile_bottom_navigation_bar.dart | 2 -- .../widgets/settings_popup_menu.dart | 3 --- .../notifications/widgets/slide_actions.dart | 4 ---- .../presentation/notifications/widgets/tab.dart | 1 - .../setting/support_setting_group.dart | 1 - .../workspace/invite_members_screen.dart | 7 ------- .../message/ai_message_action_bar.dart | 1 - .../presentation/message/ai_message_bubble.dart | 1 - .../presentation/message/message_util.dart | 2 -- .../document/application/document_bloc.dart | 1 - .../ai/ai_writer_toolbar_item.dart | 2 -- .../code_block/code_block_copy_button.dart | 1 - .../copy_and_paste/paste_from_image.dart | 3 --- .../desktop_toolbar/link/link_hover_menu.dart | 1 - .../error/error_block_component_builder.dart | 1 - .../editor_plugins/file/file_util.dart | 5 ----- .../custom_image_block_component.dart | 2 -- .../image_menu.dart | 2 -- .../multi_image_menu.dart | 1 - .../link_preview/link_preview_menu.dart | 1 - .../mention/child_page_transaction_handler.dart | 1 - .../lib/plugins/shared/share/export_tab.dart | 3 +-- .../lib/plugins/shared/share/publish_tab.dart | 9 --------- .../lib/plugins/shared/share/share_button.dart | 2 -- .../lib/plugins/shared/share/share_tab.dart | 1 - .../lib/shared/flowy_error_page.dart | 3 +-- .../lib/startup/tasks/appflowy_cloud_task.dart | 2 +- .../helpers/handle_open_workspace_error.dart | 4 ++-- .../widgets/magic_link_sign_in_buttons.dart | 2 +- .../lib/util/share_log_files.dart | 6 +++--- .../settings/appearance/appearance_cubit.dart | 17 ++++++++++++++++- .../sidebar/workspace/sidebar_workspace.dart | 2 +- .../pages/account/account_deletion.dart | 8 ++++---- .../pages/settings_manage_data_view.dart | 2 +- .../settings/pages/sites/constants.dart | 1 - .../pages/sites/domain/domain_item.dart | 2 -- .../sites/domain/domain_settings_dialog.dart | 2 -- .../published_view_settings_dialog.dart | 2 -- .../pages/sites/settings_sites_view.dart | 7 ------- .../presentation/settings/settings_dialog.dart | 4 ---- .../workspace/presentation/widgets/dialogs.dart | 4 +--- .../widgets/float_bubble/version_section.dart | 1 - .../presentation/widgets/view_title_bar.dart | 1 - frontend/resources/translations/en.json | 3 ++- 52 files changed, 36 insertions(+), 122 deletions(-) diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index a27ab07e9d..fa61e7f5b8 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -133,7 +133,6 @@ Future _afLaunchLocalUri( }; if (context != null && context.mounted) { showToastNotification( - context, message: message, type: result.type == ResultType.done ? ToastificationType.success diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 792679daf1..318b06394a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -336,7 +336,6 @@ class _MobileViewPageState extends State { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); @@ -366,7 +365,6 @@ class _MobileViewPageState extends State { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index 080d83e83c..6609b10136 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -161,7 +161,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { context.pop(); showToastNotification( - context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); } @@ -170,7 +169,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( - context, message: LocaleKeys.button_favoriteSuccessfully.tr(), ); } @@ -179,7 +177,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( - context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); } @@ -202,7 +199,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } @@ -234,12 +230,10 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkSuccess.tr(), ); } else { showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(), type: ToastificationType.error, ); @@ -323,11 +317,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { if (state.publishResult != null) { state.publishResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.publish_publishSuccessfully.tr(), ), (error) => showToastNotification( - context, message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', type: ToastificationType.error, ), @@ -335,11 +327,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { } else if (state.unpublishResult != null) { state.unpublishResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ), (error) => showToastNotification( - context, message: LocaleKeys.publish_unpublishFailed.tr(), description: error.msg, type: ToastificationType.error, @@ -349,7 +339,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { state.updatePathNameResult!.onSuccess( (value) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart index c1129af79d..86021ea938 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart @@ -65,7 +65,6 @@ class _MobileViewItemBottomSheetState extends State { Navigator.pop(context); context.read().add(const ViewEvent.duplicate()); showToastNotification( - context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); break; @@ -84,7 +83,6 @@ class _MobileViewItemBottomSheetState extends State { .read() .add(FavoriteEvent.toggle(widget.view)); showToastNotification( - context, message: !widget.view.isFavorite ? LocaleKeys.button_favoriteSuccessfully.tr() : LocaleKeys.button_unfavoriteSuccessfully.tr(), @@ -146,7 +144,6 @@ class _MobileViewItemBottomSheetState extends State { Navigator.pop(context); showToastNotification( - context, message: LocaleKeys.sideBar_removeSuccess.tr(), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index cb840b0f40..d4b4292443 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -45,7 +45,6 @@ enum MobilePaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); @@ -61,7 +60,6 @@ enum MobilePaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys.button_favoriteSuccessfully.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart index fa3494002d..b0f21188cd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart @@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State { Log.info('Open row page(${widget.documentId})'); if (view == null) { - showToastNotification(context, message: 'Failed to open row page'); + showToastNotification(message: 'Failed to open row page'); // reload the view again unawaited(_preloadView(context)); Log.error('Failed to open row page(${widget.documentId})'); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 2d409f58b6..1ae5d881ce 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> { } if (message != null) { - showToastNotification(context, message: message, type: toastType); + showToastNotification(message: message, type: toastType); } } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart index 0197f34940..485e07a28c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart @@ -339,7 +339,6 @@ class _SpaceMenuItemTrailingState extends State { context.read().add(const SpaceEvent.duplicate()); showToastNotification( - context, message: LocaleKeys.space_success_duplicateSpace.tr(), ); @@ -374,7 +373,6 @@ class _SpaceMenuItemTrailingState extends State { .add(SpaceEvent.rename(space: widget.space, name: name)); showToastNotification( - context, message: LocaleKeys.space_success_renameSpace.tr(), ); }, @@ -424,7 +422,6 @@ class _SpaceMenuItemTrailingState extends State { ); showToastNotification( - context, message: LocaleKeys.space_success_updateSpace.tr(), ); @@ -457,7 +454,6 @@ class _SpaceMenuItemTrailingState extends State { context.read().add(SpaceEvent.delete(widget.space)); showToastNotification( - context, message: LocaleKeys.space_success_deleteSpace.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 170ef46ac2..3c6adb8627 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -332,7 +332,6 @@ class _NotificationNavigationBar extends StatelessWidget { } showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -350,7 +349,6 @@ class _NotificationNavigationBar extends StatelessWidget { } showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess .tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart index dfa277f2ef..e694f9932d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart @@ -108,7 +108,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onMarkAllAsRead(BuildContext context) { showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -119,7 +118,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onArchiveAll(BuildContext context) { showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess .tr(), ); @@ -133,7 +131,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { } showToastNotification( - context, message: 'Unarchive all success (Debug Mode)', ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart index 85f468c76c..d1216eed98 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart @@ -31,7 +31,6 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_success .tr(), @@ -55,7 +54,6 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: 'Unarchive notification success', ); @@ -168,7 +166,6 @@ class _NotificationMoreActions extends StatelessWidget { Navigator.of(context).pop(); showToastNotification( - context, message: LocaleKeys.settings_notifications_markAsReadNotifications_success .tr(), ); @@ -191,7 +188,6 @@ class _NotificationMoreActions extends StatelessWidget { void _onArchive(BuildContext context) { showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_success .tr() .tr(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart index 7dda8f0a14..45e801e07c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart @@ -74,7 +74,6 @@ class _NotificationTabState extends State if (context.mounted) { showToastNotification( - context, message: LocaleKeys.settings_notifications_refreshSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 584b867736..e5e4efef77 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget { ); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.settings_files_clearCacheSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart index 2e805c5c5a..4f16aabffd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart @@ -201,7 +201,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -218,7 +217,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( - context, type: ToastificationType.error, bottomPadding: keyboardHeight, message: message, @@ -229,7 +227,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -247,7 +244,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( - context, type: ToastificationType.error, message: message, bottomPadding: keyboardHeight, @@ -258,7 +254,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceSuccess .tr(), @@ -267,7 +262,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { }, (f) { showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceFailed @@ -283,7 +277,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { final email = emailController.text; if (!isEmail(email)) { return showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 945613490a..08fd82188d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -184,7 +184,6 @@ class CopyButton extends StatelessWidget { ); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index 3ce80e2919..2786799520 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -376,7 +376,6 @@ class ChatAIMessagePopup extends StatelessWidget { } if (context.mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart index 1b0084c77c..652fe3791b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart @@ -14,7 +14,6 @@ import 'package:universal_platform/universal_platform.dart'; void openPageFromMessage(BuildContext context, ViewPB? view) { if (view == null) { showToastNotification( - context, message: LocaleKeys.chat_openPagePreviewFailedToast.tr(), type: ToastificationType.error, ); @@ -36,7 +35,6 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) { return; } showToastNotification( - context, richMessage: TextSpan( children: [ TextSpan( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 0a54ec5753..010dae1f12 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -442,7 +442,6 @@ class DocumentBloc extends Bloc { final context = AppGlobals.rootNavKey.currentContext; if (context != null && context.mounted) { showToastNotification( - context, message: 'document integrity check failed', type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 25502fd975..35575fe85a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -150,7 +150,6 @@ class _AiWriterToolbarActionListState extends State { }); } else { showToastNotification( - context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } @@ -196,7 +195,6 @@ class ImproveWritingButton extends StatelessWidget { _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { showToastNotification( - context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart index cc80119cb9..645de3b2f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart @@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget { if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index 6e6c9b1772..d086f36bed 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -73,7 +73,6 @@ extension PasteFromImage on EditorState { Log.info('unsupported format: $format'); if (UniversalPlatform.isMobile) { showToastNotification( - context, message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), ); } @@ -112,7 +111,6 @@ extension PasteFromImage on EditorState { if (errorMessage != null && context.mounted) { showToastNotification( - context, message: errorMessage, ); return false; @@ -131,7 +129,6 @@ extension PasteFromImage on EditorState { Log.error('cannot copy image file', e); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index 0d71611516..1abf70028f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -241,7 +241,6 @@ class _LinkHoverTriggerState extends State { .setData(ClipboardServiceData(plainText: href)); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 4394ff57c6..6c09ca6a28 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State void _copyBlockContent() { showToastNotification( - context, message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index 3ab93b4c95..d7ae7c5ccf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -105,7 +105,6 @@ Future downloadMediaFile( } else { if (userProfile == null) { return showToastNotification( - context, message: LocaleKeys.grid_media_downloadFailedToken.tr(), ); } @@ -128,14 +127,12 @@ Future downloadMediaFile( if (result != null && context.mounted) { showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys.grid_media_downloadSuccess.tr(), ); } } else if (context.mounted) { showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), ); @@ -159,13 +156,11 @@ Future downloadMediaFile( if (context.mounted) { showToastNotification( - context, message: LocaleKeys.grid_media_downloadSuccess.tr(), ); } } else if (context.mounted) { showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index bbb9dc2abc..7f0105134d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -378,7 +378,6 @@ class CustomImageBlockComponentState extends State onTap: () async { context.pop(); showToastNotification( - context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); await getIt().setPlainText(url); @@ -431,7 +430,6 @@ class CustomImageBlockComponentState extends State ); if (mounted) { showToastNotification( - context, message: result.isSuccess ? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr() : LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index 4a6260d8b8..d11d943066 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -117,14 +117,12 @@ class _ImageMenuState extends State { if (mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } } catch (e) { if (mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_fail.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart index b98e05f231..dc95054e81 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart @@ -218,7 +218,6 @@ class _MultiImageMenuState extends State { ClipboardData(text: images[widget.indexNotifier.value].url), ); showToastNotification( - context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index cf7d72cc2a..2ba67a87da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -78,7 +78,6 @@ class _LinkPreviewMenuState extends State { if (url != null) { Clipboard.setData(ClipboardData(text: url)); showToastNotification( - context, message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart index 117e09e67f..77f8c8d0a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart @@ -100,7 +100,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { Log.error(error); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage .tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index e4a23b6459..9d6adee7df 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -174,11 +174,10 @@ class ExportTab extends StatelessWidget { ClipboardServiceData(plainText: markdown), ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); }, - (error) => showToastNotification(context, message: error.msg), + (error) => showToastNotification(message: error.msg), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 0ffa22a43b..244ded0bf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -85,11 +85,9 @@ class PublishTab extends StatelessWidget { if (state.publishResult != null) { state.publishResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.publish_publishSuccessfully.tr(), ), (error) => showToastNotification( - context, message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', type: ToastificationType.error, ), @@ -97,11 +95,9 @@ class PublishTab extends StatelessWidget { } else if (state.unpublishResult != null) { state.unpublishResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ), (error) => showToastNotification( - context, message: LocaleKeys.publish_unpublishFailed.tr(), description: error.msg, type: ToastificationType.error, @@ -110,14 +106,12 @@ class PublishTab extends StatelessWidget { } else if (state.updatePathNameResult != null) { state.updatePathNameResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ), (error) { Log.error('update path name failed: $error'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: error.code.publishErrorMessage, @@ -182,7 +176,6 @@ class _PublishedWidgetState extends State<_PublishedWidget> { ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); }, @@ -292,7 +285,6 @@ class _PublishWidgetState extends State<_PublishWidget> { // check if any database is selected if (_selectedViews.isEmpty) { showToastNotification( - context, message: LocaleKeys.publish_noDatabaseSelected.tr(), ); return; @@ -611,7 +603,6 @@ class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { // unable to deselect the primary database if (isPrimaryDatabase) { showToastNotification( - context, message: LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart index 59ee55b980..9020441b4e 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -70,7 +70,6 @@ class ShareButton extends StatelessWidget { case ShareType.html: case ShareType.csv: showToastNotification( - context, message: LocaleKeys.settings_files_exportFileSuccess.tr(), ); break; @@ -81,7 +80,6 @@ class ShareButton extends StatelessWidget { void _handleExportError(BuildContext context, FlowyError error) { showToastNotification( - context, message: '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}', ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart index 1b925cfec6..190fe9ddd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart @@ -117,7 +117,6 @@ class _ShareTabContent extends StatelessWidget { ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart index da9f679f56..5c5e788ddb 100644 --- a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart +++ b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart @@ -45,7 +45,6 @@ class _MobileSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); @@ -101,7 +100,7 @@ class _DesktopSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( - context, + message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 2c22b8a01e..18baef4cb8 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -129,7 +129,7 @@ class AppFlowyCloudDeepLink { final context = AppGlobals.rootNavKey.currentState?.context; if (context != null) { showToastNotification( - context, + message: err.msg, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index 2e8e4feeae..51c693843f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -17,14 +17,14 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { case ErrorCode.InvalidEncryptSecret: case ErrorCode.NetworkError: showToastNotification( - context, + message: error.msg, type: ToastificationType.error, ); break; default: showToastNotification( - context, + message: error.msg, type: ToastificationType.error, callbacks: ToastificationCallbacks( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 0486d67838..69c9689823 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -65,7 +65,7 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { return showToastNotification( - context, + message: LocaleKeys.signIn_invalidEmail.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/util/share_log_files.dart b/frontend/appflowy_flutter/lib/util/share_log_files.dart index d7e7b6ce87..0435c5fe52 100644 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ b/frontend/appflowy_flutter/lib/util/share_log_files.dart @@ -25,7 +25,7 @@ Future shareLogFiles(BuildContext? context) async { if (archiveLogFiles.isEmpty) { if (context != null && context.mounted) { showToastNotification( - context, + message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -42,7 +42,7 @@ Future shareLogFiles(BuildContext? context) async { if (zip == null) { if (context != null && context.mounted) { showToastNotification( - context, + message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -72,7 +72,7 @@ Future shareLogFiles(BuildContext? context) async { } catch (e) { if (context != null && context.mounted) { showToastNotification( - context, + message: e.toString(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index b64ef7d5b8..99b9eaa2c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -17,6 +19,7 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:universal_platform/universal_platform.dart'; part 'appearance_cubit.freezed.dart'; @@ -97,7 +100,19 @@ class AppearanceSettingsCubit extends Cubit { Future setTheme(String themeName) async { _appearanceSettings.theme = themeName; unawaited(_saveAppearanceSettings()); - emit(state.copyWith(appTheme: await AppTheme.fromName(themeName))); + try { + final theme = await AppTheme.fromName(themeName); + emit(state.copyWith(appTheme: theme)); + } catch (e) { + Log.error("Error setting theme: $e"); + if (UniversalPlatform.isMacOS) { + showToastNotification( + message: + LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(), + type: ToastificationType.error, + ); + } + } } /// Reset the current user selected theme back to the default diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 038ec9f5a6..20160d32ec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State { if (message != null) { showToastNotification( - context, + message: message, type: result.fold( (_) => ToastificationType.success, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 31139492ad..8cabffc6e3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -193,7 +193,7 @@ Future deleteMyAccount( if (!isChecked) { showToastNotification( - context, + type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -208,7 +208,7 @@ Future deleteMyAccount( if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) { showToastNotification( - context, + type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -226,7 +226,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - context, + message: LocaleKeys .newSettings_myAccount_deleteAccount_deleteAccountSuccess .tr(), @@ -245,7 +245,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - context, + type: ToastificationType.error, bottomPadding: bottomPadding, message: f.msg, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 7c096b4b2f..194ad02beb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget { if (context.mounted) { showToastNotification( - context, + message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart index 1a3b305c0b..2f03fc052c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart @@ -53,7 +53,6 @@ class SettingsPageSitesEvent { ); getIt().setData(ClipboardServiceData(plainText: url)); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart index 8f9df5f1b6..b1d9b9cdae 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart @@ -253,7 +253,6 @@ class _FreePlanUpgradeButton extends StatelessWidget { onTap: () { if (isOwner) { showToastNotification( - context, message: LocaleKeys.settings_sites_namespace_redirectToPayment.tr(), type: ToastificationType.info, @@ -264,7 +263,6 @@ class _FreePlanUpgradeButton extends StatelessWidget { ); } else { showToastNotification( - context, message: LocaleKeys .settings_sites_namespace_pleaseAskOwnerToSetHomePage .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart index 6555494144..9617f2c8d6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart @@ -216,7 +216,6 @@ class _DomainSettingsDialogState extends State { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(), ); @@ -234,7 +233,6 @@ class _DomainSettingsDialogState extends State { Log.error('Failed to update namespace: $f'); showToastNotification( - context, message: basicErrorMessage, type: ToastificationType.error, description: errorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart index b7f3cecebf..ad37bae866 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart @@ -203,7 +203,6 @@ class _PublishedViewSettingsDialogState result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ); Navigator.of(context).pop(); @@ -212,7 +211,6 @@ class _PublishedViewSettingsDialogState Log.error('update path name failed: $f'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: f.code.publishErrorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart index 7b00e652ed..f3845b0896 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart @@ -178,7 +178,6 @@ class _SettingsSitesPageView extends StatelessWidget { Log.error('Failed to generate payment link for Pro Plan: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(), type: ToastificationType.error, @@ -188,14 +187,12 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((_) { showToastNotification( - context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ); }, (f) { Log.error('Failed to unpublish view: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.publish_unpublishFailed.tr(), type: ToastificationType.error, description: f.msg, @@ -204,14 +201,12 @@ class _SettingsSitesPageView extends StatelessWidget { } else if (type == SettingsSitesActionType.setHomePage && result != null) { result.fold((s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(), ); }, (f) { Log.error('Failed to set homepage: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(), type: ToastificationType.error, ); @@ -220,14 +215,12 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(), ); }, (f) { Log.error('Failed to remove homepage: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 2cf83276b4..08747b95da 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -363,7 +363,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { }) async { if (cloudUrl.isEmpty || webUrl.isEmpty) { showToastNotification( - context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -375,7 +374,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { if (mounted) { if (isValid) { showToastNotification( - context, message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]), ); @@ -387,7 +385,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { await runAppFlowy(); } else { showToastNotification( - context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -522,7 +519,6 @@ class _SupportSettings extends StatelessWidget { await getIt().clearAllCache(); if (context.mounted) { showToastNotification( - context, message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index aa541b902c..3bcc840582 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -157,7 +157,6 @@ class _NavigatorTextFieldDialogState extends State { onOkPressed: () { if (newValue.isEmpty) { showToastNotification( - context, message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), ); return; @@ -363,8 +362,7 @@ class OkCancelButton extends StatelessWidget { } } -void showToastNotification( - BuildContext context, { +void showToastNotification({ String? message, TextSpan? richMessage, String? description, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart index 923f695188..f6a2caa5a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart @@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell { } enableDocumentInternalLog = !enableDocumentInternalLog; showToastNotification( - context, message: enableDocumentInternalLog ? 'Enabled Internal Log' : 'Disabled Internal Log', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 04ae9b30ad..3be0973123 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 47a2bec391..855a715032 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -628,7 +628,8 @@ "theme": { "title": "Theme", "description": "Select a preset theme, or upload your own custom theme.", - "uploadCustomThemeTooltip": "Upload a custom theme" + "uploadCustomThemeTooltip": "Upload a custom theme", + "failedToLoadThemes": "Failed to load themes, please check your permission settings in System Settings > Privacy and Security > Files and Folders > @:appName" }, "workspaceFont": { "title": "Workspace font", From 89525b7f7b4e5e7b4b21bd99af3f776015242158 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:11:33 +0800 Subject: [PATCH 293/384] chore: bump version (#7716) --- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 0b23c5df26..41fdffb1af 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.8" +APPFLOWY_VERSION = "0.8.9" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0dc9ec673c..9b0987d0b2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -2,13 +2,13 @@ name: appflowy description: Bring projects, wikis, and teams together with AI. AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data. The best open source alternative to Notion. -publish_to: "none" +publish_to: 'none' -version: 0.8.8 +version: 0.8.9 environment: - flutter: ">=3.27.4" - sdk: ">=3.3.0 <4.0.0" + flutter: '>=3.27.4' + sdk: '>=3.3.0 <4.0.0' dependencies: any_date: ^1.0.4 @@ -38,7 +38,7 @@ dependencies: calendar_view: git: url: https://github.com/Xazin/flutter_calendar_view - ref: "6fe0c98" + ref: '6fe0c98' collection: ^1.17.1 connectivity_plus: ^5.0.2 cross_file: ^0.3.4+1 @@ -74,7 +74,7 @@ dependencies: flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "355aa56" + ref: '355aa56' flutter_math_fork: ^0.7.3 flutter_slidable: ^3.0.0 @@ -184,13 +184,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "361b99c38370abeeb19656f89e8c31cb3666623b" + ref: '361b99c38370abeeb19656f89e8c31cb3666623b' appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git - path: "packages/appflowy_editor_plugins" - ref: "4efcff7" + path: 'packages/appflowy_editor_plugins' + ref: '4efcff7' sheet: git: From 9ec04376734cdf225f3cde02b9507b92fa6039c9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 14:15:37 +0800 Subject: [PATCH 294/384] chore: remove enable sync log toggle --- .../presentation/settings/widgets/setting_appflowy_cloud.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index f423156025..5f158f4ae1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget { const VSpace(8), const AppFlowyCloudEnableSync(), const VSpace(6), - const AppFlowyCloudSyncLogEnabled(), + // const AppFlowyCloudSyncLogEnabled(), const VSpace(12), RestartButton( onClick: () { From 2371d4691df487f65b2dda0bd33402448275fbd9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 20:07:06 +0800 Subject: [PATCH 295/384] chore: fix local ai setting --- .../settings/pages/setting_ai_view/local_ai_setting.dart | 6 ++---- .../pages/setting_ai_view/plugin_status_indicator.dart | 4 ++++ frontend/resources/translations/en.json | 4 ++-- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 9 +++++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 751e7a6180..b836f15b03 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -140,10 +140,8 @@ class LocalAISettingPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const LocalAIStatusIndicator(), - if (state.showSettings) ...[ - const VSpace(10), - OllamaSettingPage(), - ], + const VSpace(10), + OllamaSettingPage(), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart index fdf03bb9ca..a280cf0644 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart @@ -288,6 +288,10 @@ class _LackOfResource extends StatelessWidget { text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), style: textStyle, ), + TextSpan( + text: modelNames.join(', '), + style: textStyle, + ), TextSpan( text: ' ', style: textStyle, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 855a715032..43a59974fd 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -885,7 +885,7 @@ "pleaseFollowThese": "Please follow these", "instructions": "instructions", "installOllamaLai": "to set up Ollama and AppFlowy Local AI.", - "modelsMissing": "Cannot find the required models.", + "modelsMissing": "Cannot find the required models: ", "downloadModel": "to download them." } }, @@ -3209,4 +3209,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index b384b30f4a..f90c360ec7 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -195,9 +195,14 @@ impl LocalAIResourceController { let tags: TagsResponse = resp.json().await.inspect_err(|e| { log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") })?; - // Check each required model is present in the response. + // Check if each of our required models exists in the list of available models + trace!("[LLM Resource] ollama available models: {:?}", tags.models); for required in &required_models { - if !tags.models.iter().any(|m| m.name.contains(required)) { + if !tags + .models + .iter() + .any(|m| m.name == *required || m.name == format!("{}:latest", required)) + { log::trace!( "[LLM Resource] required model '{}' not found in API response", required From a7adeeadb17863630933490e119a5fe8ef08bd88 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 20:47:03 +0800 Subject: [PATCH 296/384] chore: remove local ai related path to lai repo --- frontend/rust-lib/Cargo.lock | 10 +- frontend/rust-lib/Cargo.toml | 6 +- frontend/rust-lib/flowy-ai/Cargo.toml | 5 - .../flowy-ai/src/local_ai/controller.rs | 2 +- .../flowy-ai/src/local_ai/resource.rs | 2 +- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 133 +----------------- 6 files changed, 9 insertions(+), 149 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a72f4596f6..a251594ef9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,12 +345,11 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "af-plugin", "anyhow", "bytes", - "futures", "reqwest 0.11.27", "serde", "serde_json", @@ -358,14 +357,12 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "zip 2.2.0", - "zip-extensions", ] [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "anyhow", "futures-util", @@ -379,7 +376,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "anyhow", "cfg-if", @@ -2504,7 +2501,6 @@ dependencies = [ "tracing-subscriber", "uuid", "validator 0.18.1", - "winreg 0.55.0", "zip 2.2.0", "zip-extensions", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 78f67f8f90..4df29d1fe4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index ee056021c4..a08eae7e92 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -53,11 +53,6 @@ collab-integrate.workspace = true notify = "6.1.1" af-mcp = { version = "0.1.0" } -[target.'cfg(target_os = "windows")'.dependencies] -winreg = "0.55" - -#cmd_lib = { version = "1.9.5" } - [dev-dependencies] dotenv = "0.15.0" uuid.workspace = true diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 6fbe7ce2fc..ac44e9ad55 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -13,9 +13,9 @@ use futures::Sink; use lib_infra::async_trait::async_trait; use std::collections::HashMap; -use crate::local_ai::watch::is_plugin_ready; use crate::stream_message::StreamMessage; use af_local_ai::ollama_plugin::OllamaAIPlugin; +use af_plugin::core::path::is_plugin_ready; use af_plugin::core::plugin::RunningState; use arc_swap::ArcSwapOption; use futures_util::SinkExt; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index f90c360ec7..2fc6fad2cc 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,13 +5,13 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -use crate::local_ai::watch::{is_plugin_ready, ollama_plugin_path}; #[cfg(target_os = "macos")] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use af_local_ai::ollama_plugin::OllamaPluginConfig; +use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path}; use lib_infra::util::{get_operating_system, OperatingSystem}; use reqwest::Client; use serde::Deserialize; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 1d7be0daf9..2baed3f0a5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -1,13 +1,10 @@ use crate::local_ai::resource::WatchDiskEvent; +use af_plugin::core::path::{install_path, ollama_plugin_path}; use flowy_error::{FlowyError, FlowyResult}; use std::path::PathBuf; -use std::process::Command; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::{error, trace}; -#[cfg(windows)] -use winreg::{enums::*, RegKey}; - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[allow(dead_code)] pub struct WatchContext { @@ -61,131 +58,3 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver Option { - None -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn install_path() -> Option { - #[cfg(target_os = "windows")] - return None; - - #[cfg(target_os = "macos")] - return Some(PathBuf::from("/usr/local/bin")); - - #[cfg(target_os = "linux")] - return None; -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub fn is_plugin_ready() -> bool { - ollama_plugin_path().exists() || ollama_plugin_command_available() -} - -#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub fn is_plugin_ready() -> bool { - false -} - -#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub(crate) fn ollama_plugin_path() -> PathBuf { - PathBuf::new() -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { - #[cfg(target_os = "windows")] - { - // Use LOCALAPPDATA for a user-specific installation path on Windows. - let local_appdata = - std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Program Files".to_string()); - std::path::PathBuf::from(local_appdata).join("Programs\\appflowy_plugin\\af_ollama_plugin.exe") - } - - #[cfg(target_os = "macos")] - { - let offline_app = "af_ollama_plugin"; - std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) - } - - #[cfg(target_os = "linux")] - { - let offline_app = "af_ollama_plugin"; - std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) - } -} - -pub(crate) fn ollama_plugin_command_available() -> bool { - if cfg!(windows) { - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let output = Command::new("cmd") - .args(&["/C", "where", "af_ollama_plugin"]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - if let Ok(output) = output { - if !output.stdout.is_empty() { - return true; - } - } - - // 2. Fallback: Check registry PATH for the executable - let path_dirs = get_windows_path_dirs(); - let plugin_exe = "af_ollama_plugin.exe"; // Adjust name if needed - - path_dirs.iter().any(|dir| { - let full_path = std::path::Path::new(dir).join(plugin_exe); - full_path.exists() - }) - } - - #[cfg(not(windows))] - false - } else { - let output = Command::new("command") - .args(["-v", "af_ollama_plugin"]) - .output(); - match output { - Ok(o) => !o.stdout.is_empty(), - _ => false, - } - } -} - -#[cfg(windows)] -fn get_windows_path_dirs() -> Vec { - let mut paths = Vec::new(); - - // Check HKEY_CURRENT_USER\Environment - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(env) = hkcu.open_subkey("Environment") { - if let Ok(path) = env.get_value::("Path") { - paths.extend(path.split(';').map(|s| s.trim().to_string())); - } - } - - // Check HKEY_LOCAL_MACHINE\SYSTEM\...\Environment - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - if let Ok(env) = hklm.open_subkey(r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment") - { - if let Ok(path) = env.get_value::("Path") { - paths.extend(path.split(';').map(|s| s.trim().to_string())); - } - } - paths -} - -#[cfg(test)] -mod tests { - use crate::local_ai::watch::ollama_plugin_command_available; - - #[test] - fn test_command_import() { - let result = ollama_plugin_command_available(); - println!("ollama plugin exist: {:?}", result); - } -} From 403c558f9bcc4fcc86db8122e7183ba5e44ec133 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 10 Apr 2025 13:45:22 +0800 Subject: [PATCH 297/384] chore: update translation (#7675) --- frontend/resources/translations/zh-CN.json | 76 ++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index df65cbac4b..2731a0fb2e 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1326,6 +1326,62 @@ }, "document": { "selectADocumentToLinkTo": "选择要链接到的文档" + }, + "name": { + "textStyle": "文本样式", + "list": "列表", + "toggle": "切换", + "fileAndMedia": "文件与媒体", + "simpleTable": "简单表格", + "visuals": "视觉元素", + "document": "文档", + "advanced": "高级", + "text": "文本", + "heading1": "一级标题", + "heading2": "二级标题", + "heading3": "三级标题", + "image": "图片", + "bulletedList": "项目符号列表", + "numberedList": "编号列表", + "todoList": "待办事项列表", + "doc": "文档", + "linkedDoc": "链接到页面", + "grid": "网格", + "linkedGrid": "链接网格", + "kanban": "看板", + "linkedKanban": "链接看板", + "calendar": "日历", + "linkedCalendar": "链接日历", + "quote": "引用", + "divider": "分隔符", + "table": "表格", + "callout": "提示框", + "outline": "大纲", + "mathEquation": "数学公式", + "code": "代码", + "toggleList": "切换列表", + "toggleHeading1": "切换标题1", + "toggleHeading2": "切换标题2", + "toggleHeading3": "切换标题3", + "emoji": "表情符号", + "aiWriter": "向AI提问", + "dateOrReminder": "日期或提醒", + "photoGallery": "图片库", + "file": "文件", + "twoColumns": "两列", + "threeColumns": "三列", + "fourColumns": "四列" + }, + "subPage": { + "name": "文档", + "keyword1": "子页面", + "keyword2": "页面", + "keyword3": "子页面", + "keyword4": "插入页面", + "keyword5": "嵌入页面", + "keyword6": "新页面", + "keyword7": "创建页面", + "keyword8": "文档" } }, "selectionMenu": { @@ -1337,6 +1393,16 @@ "referencedGrid": "引用的网格", "referencedCalendar": "引用的日历", "referencedDocument": "参考文档", + "aiWriter": { + "userQuestion": "向AI提问", + "continueWriting": "继续写作", + "fixSpelling": "修正拼写和语法", + "improveWriting": "优化写作", + "summarize": "总结", + "explain": "解释", + "makeShorter": "缩短", + "makeLonger": "扩展" + }, "autoGeneratorMenuItemName": "AI 创作", "autoGeneratorTitleName": "AI: 让 AI 写些什么...", "autoGeneratorLearnMore": "学习更多", @@ -1397,7 +1463,7 @@ }, "optionAction": { "click": "点击", - "toOpenMenu": " 来打开菜单", + "toOpenMenu": "打开菜单", "delete": "删除", "duplicate": "复制", "turnInto": "变成", @@ -1407,8 +1473,10 @@ "align": "对齐", "left": "左", "center": "中心", - "right": "又", - "defaultColor": "默认" + "right": "右", + "defaultColor": "默认", + "depth": "深度", + "copyLinkToBlock": "粘贴块链接" }, "image": { "addAnImage": "添加图像", @@ -2003,4 +2071,4 @@ "yesterday": "昨天", "today": "今天" } -} +} \ No newline at end of file From e3bbabd63ede2c0df0c575bf26f73aa13884d450 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 10 Apr 2025 14:06:08 +0800 Subject: [PATCH 298/384] feat: sign in with passcode (#7685) * feat: sign in with password or passcode * feat: add loading dialog * chore: update translations * feat: support otp on mobile --- .../anon_user_data_migration_test.dart | 1 - .../setting/about/about_setting_group.dart | 4 +- .../workspace/invite_members_screen.dart | 3 +- .../editor_plugins/file/file_util.dart | 3 +- .../lib/startup/tasks/app_widget.dart | 54 ++--- .../startup/tasks/appflowy_cloud_task.dart | 58 ++++++ .../auth/af_cloud_auth_service.dart | 20 +- .../auth/af_cloud_mock_auth_service.dart | 11 +- .../user/application/auth/auth_service.dart | 16 +- .../auth/backend_auth_service.dart | 14 +- .../lib/user/application/sign_in_bloc.dart | 141 ++++++++++--- .../lib/user/application/user_service.dart | 9 + .../desktop_sign_in_screen.dart | 72 ++++--- .../sign_in_screen/mobile_sign_in_screen.dart | 32 +-- .../widgets/anonymous_sign_in_button.dart | 2 +- .../anonymous_sign_in_button.dart | 16 ++ .../continue_with/continue_with_email.dart | 23 +++ .../continue_with_email_and_password.dart | 133 +++++++++++++ ...inue_with_magic_link_or_passcode_page.dart | 187 ++++++++++++++++++ .../continue_with/continue_with_password.dart | 21 ++ .../continue_with_password_page.dart | 147 ++++++++++++++ .../sign_in_screen/widgets/logo/logo.dart | 20 ++ .../widgets/magic_link_sign_in_buttons.dart | 8 +- .../widgets/sign_in_agreement.dart | 32 +-- .../widgets/sign_in_anonymous_button.dart | 109 ++-------- .../widgets/sign_in_or_logout_button.dart | 57 ++---- .../third_party_sign_in_button.dart | 124 +++--------- .../third_party_sign_in_buttons.dart | 86 ++++---- .../sign_in_screen/widgets/widgets.dart | 8 +- .../widgets/auth_form_container.dart | 2 +- .../widgets/flowy_logo_title.dart | 24 +-- .../sidebar/billing/sidebar_plan_bloc.dart | 3 + .../pages/account/account_sign_in_out.dart | 4 +- .../presentation/widgets/dialogs.dart | 4 +- .../packages/appflowy_ui/README.md | 2 +- .../packages/appflowy_ui/lib/appflowy_ui.dart | 2 - .../lib/src/theme/data/builder.dart | 2 +- frontend/appflowy_flutter/pubspec.lock | 9 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- frontend/resources/translations/en.json | 12 +- frontend/rust-lib/.vscode/launch.json | 17 ++ frontend/rust-lib/Cargo.lock | 24 +-- frontend/rust-lib/Cargo.toml | 4 +- .../af_cloud/impls/user/cloud_service_impl.rs | 24 ++- .../src/local_server/impls/user.rs | 11 +- .../flowy-server/src/supabase/api/user.rs | 10 + frontend/rust-lib/flowy-user-pub/src/cloud.rs | 9 +- .../rust-lib/flowy-user/src/entities/auth.rs | 48 +++++ .../rust-lib/flowy-user/src/event_handler.rs | 23 ++- frontend/rust-lib/flowy-user/src/event_map.rs | 7 +- .../flowy-user/src/user_manager/manager.rs | 27 +++ 51 files changed, 1227 insertions(+), 455 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart rename frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/{ => third_party_sign_in_button}/third_party_sign_in_button.dart (53%) rename frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/{ => third_party_sign_in_button}/third_party_sign_in_buttons.dart (68%) create mode 100644 frontend/rust-lib/.vscode/launch.json diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart index 230ee59495..e34ac02aab 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart @@ -15,7 +15,6 @@ void main() { cloudType: AuthenticatorType.appflowyCloudSelfHost, ); - await tester.tapContinousAnotherWay(); await tester.tapAnonymousSignInButton(); await tester.expectToSeeHomePageWithGetStartedPage(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index d4f0766626..2d5a3176cd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget { trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.io/privacy'), + onTap: () => afLaunchUrlString('https://appflowy.com/privacy'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_termsAndConditions.tr(), trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.io/terms'), + onTap: () => afLaunchUrlString('https://appflowy.com/terms'), ), if (kDebugMode) MobileSettingItem( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart index 4f16aabffd..62aa114ef3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart @@ -276,10 +276,11 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { void _inviteMember(BuildContext context) { final email = emailController.text; if (!isEmail(email)) { - return showToastNotification( + showToastNotification( type: ToastificationType.error, message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); + return; } context .read() diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index d7ae7c5ccf..83debdd71b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -104,9 +104,10 @@ Future downloadMediaFile( await afLaunchUrlString(file.url); } else { if (userProfile == null) { - return showToastNotification( + showToastNotification( message: LocaleKeys.grid_media_downloadFailedToken.tr(), ); + return; } final uri = Uri.parse(file.url); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index ce28ea68b0..ed4c4d1623 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -19,6 +19,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -98,7 +99,7 @@ class InitAppWidgetTask extends LaunchTask { Locale('zh', 'TW'), Locale('fa'), Locale('hin'), - Locale('mr','IN'), + Locale('mr', 'IN'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), @@ -224,31 +225,36 @@ class _ApplicationWidgetState extends State { Tooltip.dismissAllToolTips(); } }, - child: MaterialApp.router( - builder: (context, child) => MediaQuery( - // use the 1.0 as the textScaleFactor to avoid the text size - // affected by the system setting. - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(state.textScaleFactor), - ), - child: overlayManagerBuilder( - context, - !UniversalPlatform.isMobile && FeatureFlag.search.isOn - ? CommandPalette( - notifier: _commandPaletteNotifier, - child: child, - ) - : child, + child: AppFlowyTheme( + data: Theme.of(context).brightness == Brightness.light + ? AppFlowyThemeData.light() + : AppFlowyThemeData.dark(), + child: MaterialApp.router( + builder: (context, child) => MediaQuery( + // use the 1.0 as the textScaleFactor to avoid the text size + // affected by the system setting. + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(state.textScaleFactor), + ), + child: overlayManagerBuilder( + context, + !UniversalPlatform.isMobile && FeatureFlag.search.isOn + ? CommandPalette( + notifier: _commandPaletteNotifier, + child: child, + ) + : child, + ), ), + debugShowCheckedModeBanner: false, + theme: state.lightTheme, + darkTheme: state.darkTheme, + themeMode: state.themeMode, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: state.locale, + routerConfig: routerConfig, ), - debugShowCheckedModeBanner: false, - theme: state.lightTheme, - darkTheme: state.darkTheme, - themeMode: state.themeMode, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: state.locale, - routerConfig: routerConfig, ), ), ), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 18baef4cb8..d8dc4dd121 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -83,6 +83,13 @@ class AppFlowyCloudDeepLink { void unsubscribeDeepLinkLoadingState(VoidCallback listener) => _stateNotifier?.removeListener(listener); + Future passGotrueTokenResponse( + GotrueTokenResponsePB gotrueTokenResponse, + ) async { + final uri = _buildDeepLinkUri(gotrueTokenResponse); + await _handleUri(uri); + } + Future _handleUri( Uri? uri, ) async { @@ -173,6 +180,57 @@ class AppFlowyCloudDeepLink { bool _isPaymentSuccessUri(Uri uri) { return uri.host == 'payment-success'; } + + Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) { + final params = {}; + + if (gotrueTokenResponse.hasAccessToken() && + gotrueTokenResponse.accessToken.isNotEmpty) { + params['access_token'] = gotrueTokenResponse.accessToken; + } + + if (gotrueTokenResponse.hasExpiresAt()) { + params['expires_at'] = gotrueTokenResponse.expiresAt.toString(); + } + + if (gotrueTokenResponse.hasExpiresIn()) { + params['expires_in'] = gotrueTokenResponse.expiresIn.toString(); + } + + if (gotrueTokenResponse.hasProviderRefreshToken() && + gotrueTokenResponse.providerRefreshToken.isNotEmpty) { + params['provider_refresh_token'] = + gotrueTokenResponse.providerRefreshToken; + } + + if (gotrueTokenResponse.hasProviderAccessToken() && + gotrueTokenResponse.providerAccessToken.isNotEmpty) { + params['provider_token'] = gotrueTokenResponse.providerAccessToken; + } + + if (gotrueTokenResponse.hasRefreshToken() && + gotrueTokenResponse.refreshToken.isNotEmpty) { + params['refresh_token'] = gotrueTokenResponse.refreshToken; + } + + if (gotrueTokenResponse.hasTokenType() && + gotrueTokenResponse.tokenType.isNotEmpty) { + params['token_type'] = gotrueTokenResponse.tokenType; + } + + if (params.isEmpty) { + return null; + } + + final fragment = params.entries + .map( + (e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}', + ) + .join('&'); + + return Uri.parse('appflowy-flutter://login-callback#$fragment'); + } } class InitAppFlowyCloudTask extends LaunchTask { diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 149bddc951..6d02f188c8 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -32,12 +32,17 @@ class AppFlowyCloudAuthService implements AuthService { } @override - Future> signInWithEmailPassword({ + Future> + signInWithEmailPassword({ required String email, required String password, Map params = const {}, }) async { - throw UnimplementedError(); + return _backendAuthService.signInWithEmailPassword( + email: email, + password: password, + params: params, + ); } @override @@ -106,6 +111,17 @@ class AppFlowyCloudAuthService implements AuthService { ); } + @override + Future> signInWithPasscode({ + required String email, + required String passcode, + }) async { + return _backendAuthService.signInWithPasscode( + email: email, + passcode: passcode, + ); + } + @override Future> getUser() async { return UserBackendService.getCurrentUserProfile(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index 5f8ea7cac6..d8cee89b59 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -33,7 +33,8 @@ class AppFlowyCloudMockAuthService implements AuthService { } @override - Future> signInWithEmailPassword({ + Future> + signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -106,4 +107,12 @@ class AppFlowyCloudMockAuthService implements AuthService { Future> getUser() async { return UserBackendService.getCurrentUserProfile(); } + + @override + Future> signInWithPasscode({ + required String email, + required String passcode, + }) async { + throw UnimplementedError(); + } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart index 90c6954afe..9879b9a18e 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; class AuthServiceMapKeys { @@ -23,7 +23,8 @@ abstract class AuthService { /// /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signInWithEmailPassword({ + Future> + signInWithEmailPassword({ required String email, required String password, Map params, @@ -75,6 +76,17 @@ abstract class AuthService { Map params, }); + /// Authenticates a user with a passcode sent to their email. + /// + /// - `email`: The email address of the user. + /// - `passcode`: The passcode of the user. + /// + /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. + Future> signInWithPasscode({ + required String email, + required String passcode, + }); + /// Signs out the currently authenticated user. Future signOut(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index 4d03788c8c..f47fd5a4a6 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -18,7 +18,8 @@ class BackendAuthService implements AuthService { final AuthenticatorPB authType; @override - Future> signInWithEmailPassword({ + Future> + signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -28,8 +29,7 @@ class BackendAuthService implements AuthService { ..password = password ..authType = authType ..deviceId = await getDeviceId(); - final response = UserEventSignInWithEmailPassword(request).send(); - return response.then((value) => value); + return UserEventSignInWithEmailPassword(request).send(); } @override @@ -105,4 +105,12 @@ class BackendAuthService implements AuthService { // No need to pass the redirect URL. return UserBackendService.signInWithMagicLink(email, ''); } + + @override + Future> signInWithPasscode({ + required String email, + required String passcode, + }) async { + return UserBackendService.signInWithPasscode(email, passcode); + } } diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 6fda156567..54a60702c8 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -30,12 +30,26 @@ class SignInBloc extends Bloc { on( (event, emit) async { await event.when( - signedInWithUserEmailAndPassword: () async => _onSignIn(emit), - signedInWithOAuth: (platform) async => - _onSignInWithOAuth(emit, platform), - signedInAsGuest: () async => _onSignInAsGuest(emit), - signedWithMagicLink: (email) async => - _onSignInWithMagicLink(emit, email), + signInWithEmailAndPassword: (email, password) async => + _onSignInWithEmailAndPassword( + emit, + email: email, + password: password, + ), + signInWithOAuth: (platform) async => _onSignInWithOAuth( + emit, + platform: platform, + ), + signInAsGuest: () async => _onSignInAsGuest(emit), + signInWithMagicLink: (email) async => _onSignInWithMagicLink( + emit, + email: email, + ), + signInWithPasscode: (email, passcode) async => _onSignInWithPasscode( + emit, + email: email, + passcode: passcode, + ), deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result), cancel: () { emit( @@ -119,26 +133,34 @@ class SignInBloc extends Bloc { } } - Future _onSignIn(Emitter emit) async { + Future _onSignInWithEmailAndPassword( + Emitter emit, { + required String email, + required String password, + }) async { final result = await authService.signInWithEmailPassword( - email: state.email ?? '', - password: state.password ?? '', + email: email, + password: password, ); emit( result.fold( - (userProfile) => state.copyWith( - isSubmitting: false, - successOrFail: FlowyResult.success(userProfile), - ), + (gotrueTokenResponse) { + getIt().passGotrueTokenResponse( + gotrueTokenResponse, + ); + return state.copyWith( + isSubmitting: false, + ); + }, (error) => _stateFromCode(error), ), ); } Future _onSignInWithOAuth( - Emitter emit, - String platform, - ) async { + Emitter emit, { + required String platform, + }) async { emit( state.copyWith( isSubmitting: true, @@ -161,9 +183,9 @@ class SignInBloc extends Bloc { } Future _onSignInWithMagicLink( - Emitter emit, - String email, - ) async { + Emitter emit, { + required String email, + }) async { emit( state.copyWith( isSubmitting: true, @@ -183,6 +205,40 @@ class SignInBloc extends Bloc { ); } + Future _onSignInWithPasscode( + Emitter emit, { + required String email, + required String passcode, + }) async { + emit( + state.copyWith( + isSubmitting: true, + emailError: null, + passwordError: null, + successOrFail: null, + ), + ); + + final result = await authService.signInWithPasscode( + email: email, + passcode: passcode, + ); + + emit( + result.fold( + (gotrueTokenResponse) { + getIt().passGotrueTokenResponse( + gotrueTokenResponse, + ); + return state.copyWith( + isSubmitting: false, + ); + }, + (error) => _stateFromCode(error), + ), + ); + } + Future _onSignInAsGuest( Emitter emit, ) async { @@ -224,10 +280,17 @@ class SignInBloc extends Bloc { emailError: null, ); case ErrorCode.UserUnauthorized: + final errorMsg = error.msg; + String msg = LocaleKeys.signIn_generalError.tr(); + if (errorMsg.contains('rate limit')) { + msg = LocaleKeys.signIn_limitRateError.tr(); + } else if (errorMsg.contains('invalid')) { + msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); + } return state.copyWith( isSubmitting: false, successOrFail: FlowyResult.failure( - FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()), + FlowyError(msg: msg), ), ); default: @@ -243,19 +306,35 @@ class SignInBloc extends Bloc { @freezed class SignInEvent with _$SignInEvent { - const factory SignInEvent.signedInWithUserEmailAndPassword() = - SignedInWithUserEmailAndPassword; - const factory SignInEvent.signedInWithOAuth(String platform) = - SignedInWithOAuth; - const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; - const factory SignInEvent.signedWithMagicLink(String email) = - SignedWithMagicLink; - const factory SignInEvent.emailChanged(String email) = EmailChanged; - const factory SignInEvent.passwordChanged(String password) = PasswordChanged; + // Sign in methods + const factory SignInEvent.signInWithEmailAndPassword({ + required String email, + required String password, + }) = SignInWithEmailAndPassword; + const factory SignInEvent.signInWithOAuth({ + required String platform, + }) = SignInWithOAuth; + const factory SignInEvent.signInAsGuest() = SignInAsGuest; + const factory SignInEvent.signInWithMagicLink({ + required String email, + }) = SignInWithMagicLink; + const factory SignInEvent.signInWithPasscode({ + required String email, + required String passcode, + }) = SignInWithPasscode; + + // Event handlers + const factory SignInEvent.emailChanged({ + required String email, + }) = EmailChanged; + const factory SignInEvent.passwordChanged({ + required String password, + }) = PasswordChanged; const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) = DeepLinkStateChange; - const factory SignInEvent.cancel() = _Cancel; - const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType; + + const factory SignInEvent.cancel() = Cancel; + const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType; } // we support sign in directly without sign up, but we want to allow the users to sign up if they want to diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 5a75a4df3e..644a115641 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -86,6 +86,15 @@ class UserBackendService implements IUserBackendService { return UserEventMagicLinkSignIn(payload).send(); } + static Future> + signInWithPasscode( + String email, + String passcode, + ) async { + final payload = PasscodeSignInPB(email: email, passcode: passcode); + return UserEventPasscodeSignIn(payload).send(); + } + static Future> signOut() { return UserEventSignOut().send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 94b4347869..3a906ee0c4 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -1,11 +1,13 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/settings/show_settings.dart'; import 'package:appflowy/shared/window_title_bar.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -19,6 +21,8 @@ class DesktopSignInScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + const indicatorMinHeight = 4.0; return BlocBuilder( builder: (context, state) { @@ -29,25 +33,23 @@ class DesktopSignInScreen extends StatelessWidget { children: [ const Spacer(), - const VSpace(20), - // logo and title FlowyLogoTitle( title: LocaleKeys.welcomeText.tr(), - logoSize: const Size(60, 60), + logoSize: Size.square(36), ), - const VSpace(20), + VSpace(theme.spacing.xxl), - // magic link sign in - const SignInWithMagicLinkButtons(), - const VSpace(20), + // continue with email and password + const ContinueWithEmailAndPassword(), + VSpace(theme.spacing.xxl), // third-party sign in. if (isAuthEnabled) ...[ const _OrDivider(), - const VSpace(20), + VSpace(theme.spacing.xxl), const ThirdPartySignInButtons(), - const VSpace(20), + VSpace(theme.spacing.xxl), ], // sign in agreement @@ -69,7 +71,7 @@ class DesktopSignInScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ DesktopSignInSettingsButton(), - HSpace(42), + HSpace(20), SignInAnonymousButtonV2(), ], ), @@ -99,18 +101,24 @@ class DesktopSignInSettingsButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.signIn_settings.tr(), - textAlign: TextAlign.center, - fontSize: 12.0, - // fontWeight: FontWeight.w500, - color: Colors.grey, - decoration: TextDecoration.underline, + final theme = AppFlowyTheme.of(context); + return AFGhostIconTextButton( + text: LocaleKeys.signIn_settings.tr(), + textColor: (context, isHovering, disabled) { + return theme.textColorScheme.secondary; + }, + size: AFButtonSize.s, + padding: EdgeInsets.symmetric( + horizontal: theme.spacing.m, + vertical: theme.spacing.xs, ), - onTap: () { - showSimpleSettingsDialog(context); + onTap: () => showSimpleSettingsDialog(context), + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.settings_s, + size: Size.square(20), + color: theme.textColorScheme.secondary, + ); }, ); } @@ -121,14 +129,30 @@ class _OrDivider extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Row( children: [ - const Flexible(child: Divider(thickness: 1)), + Flexible( + child: Divider( + thickness: 1, + color: theme.borderColorScheme.greyTertiary, + ), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - child: FlowyText.regular(LocaleKeys.signIn_or.tr()), + child: Text( + LocaleKeys.signIn_or.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + ), + Flexible( + child: Divider( + thickness: 1, + color: theme.borderColorScheme.greyTertiary, + ), ), - const Flexible(child: Divider(thickness: 1)), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 2a9a6fe798..6326e1a811 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -7,6 +7,7 @@ import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -37,7 +38,7 @@ class MobileSignInScreen extends StatelessWidget { const VSpace(spacing * 2), isLocalAuthEnabled ? const SignInAnonymousButtonV3() - : const SignInWithMagicLinkButtons(), + : const ContinueWithEmailAndPassword(), const VSpace(spacing), if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), const VSpace(spacing * 1.5), @@ -103,21 +104,28 @@ class MobileSignInScreen extends StatelessWidget { } Widget _buildSettingsButton(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( mainAxisSize: MainAxisSize.min, children: [ - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.signIn_settings.tr(), - textAlign: TextAlign.center, - fontSize: 12.0, - // fontWeight: FontWeight.w500, - color: Colors.grey, - decoration: TextDecoration.underline, + AFGhostIconTextButton( + text: LocaleKeys.signIn_settings.tr(), + textColor: (context, isHovering, disabled) { + return theme.textColorScheme.secondary; + }, + size: AFButtonSize.s, + padding: EdgeInsets.symmetric( + horizontal: theme.spacing.m, + vertical: theme.spacing.xs, ), - onTap: () { - context.push(MobileLaunchSettingsPage.routeName); + onTap: () => context.push(MobileLaunchSettingsPage.routeName), + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.settings_s, + size: Size.square(20), + color: theme.textColorScheme.secondary, + ); }, ), const HSpace(24), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart index 1e5d7fa531..c6b2d5401c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart @@ -34,7 +34,7 @@ class SignInAnonymousButtonV3 extends StatelessWidget { ? () { context .read() - .add(const SignInEvent.signedInAsGuest()); + .add(const SignInEvent.signInAsGuest()); } : () { final bloc = context.read(); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart new file mode 100644 index 0000000000..351527137f --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart @@ -0,0 +1,16 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class AnonymousSignInButton extends StatelessWidget { + const AnonymousSignInButton({super.key}); + + @override + Widget build(BuildContext context) { + return AFGhostButton.normal( + onTap: () {}, + builder: (context, isHovering, disabled) { + return const Placeholder(); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart new file mode 100644 index 0000000000..c4cf504ef5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart @@ -0,0 +1,23 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class ContinueWithEmail extends StatelessWidget { + const ContinueWithEmail({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return AFFilledTextButton.primary( + text: LocaleKeys.signIn_continueWithEmail.tr(), + size: AFButtonSize.l, + alignment: Alignment.center, + onTap: onTap, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart new file mode 100644 index 0000000000..e918c3f4f1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -0,0 +1,133 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:universal_platform/universal_platform.dart'; + +class ContinueWithEmailAndPassword extends StatefulWidget { + const ContinueWithEmailAndPassword({super.key}); + + @override + State createState() => + _ContinueWithEmailAndPasswordState(); +} + +class _ContinueWithEmailAndPasswordState + extends State { + final controller = TextEditingController(); + final focusNode = FocusNode(); + + @override + void dispose() { + controller.dispose(); + focusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Column( + children: [ + SizedBox( + height: UniversalPlatform.isMobile ? 38.0 : 40.0, + child: AFTextField( + controller: controller, + hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), + radius: 10, + onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage( + context, + value, + ), + ), + ), + VSpace(theme.spacing.l), + ContinueWithEmail( + onTap: () => _pushContinueWithMagicLinkOrPasscodePage( + context, + controller.text, + ), + ), + // Hide password sign in until we implement the reset password / forgot password + // VSpace(theme.spacing.l), + // ContinueWithPassword( + // onTap: () => _pushContinueWithPasswordPage( + // context, + // controller.text, + // ), + // ), + ], + ); + } + + void _pushContinueWithMagicLinkOrPasscodePage( + BuildContext context, + String email, + ) { + if (!isEmail(email)) { + showToastNotification( + message: LocaleKeys.signIn_invalidEmail.tr(), + type: ToastificationType.error, + ); + return; + } + + final signInBloc = context.read(); + + signInBloc.add(SignInEvent.signInWithMagicLink(email: email)); + + // push the a continue with magic link or passcode screen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: signInBloc, + child: ContinueWithMagicLinkOrPasscodePage( + email: email, + backToLogin: () => Navigator.pop(context), + onEnterPasscode: (passcode) => signInBloc.add( + SignInEvent.signInWithPasscode( + email: email, + passcode: passcode, + ), + ), + ), + ), + ), + ); + } + + // void _pushContinueWithPasswordPage( + // BuildContext context, + // String email, + // ) { + // final signInBloc = context.read(); + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => ContinueWithPasswordPage( + // email: email, + // backToLogin: () => Navigator.pop(context), + // onEnterPassword: (password) => signInBloc.add( + // SignInEvent.signInWithEmailAndPassword( + // email: email, + // password: password, + // ), + // ), + // onForgotPassword: () { + // // todo: implement forgot password + // }, + // ), + // ), + // ); + // } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart new file mode 100644 index 0000000000..ea7ff087ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -0,0 +1,187 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ContinueWithMagicLinkOrPasscodePage extends StatefulWidget { + const ContinueWithMagicLinkOrPasscodePage({ + super.key, + required this.backToLogin, + required this.email, + required this.onEnterPasscode, + }); + + final String email; + final VoidCallback backToLogin; + final ValueChanged onEnterPasscode; + + @override + State createState() => + _ContinueWithMagicLinkOrPasscodePageState(); +} + +class _ContinueWithMagicLinkOrPasscodePageState + extends State { + final passcodeController = TextEditingController(); + + bool isEnteringPasscode = false; + + ToastificationItem? toastificationItem; + + @override + void dispose() { + passcodeController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.isSubmitting) { + _showLoadingDialog(); + } else { + _dismissLoadingDialog(); + } + }, + child: Scaffold( + body: Center( + child: SizedBox( + width: 320, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo, title and description + ..._buildLogoTitleAndDescription(), + + // Enter code manually + ..._buildEnterCodeManually(), + + // Back to login + ..._buildBackToLogin(), + ], + ), + ), + ), + ), + ); + } + + List _buildEnterCodeManually() { + // todo: ask designer to provide the spacing + final spacing = VSpace(20); + + if (!isEnteringPasscode) { + return [ + AFFilledTextButton.primary( + text: LocaleKeys.signIn_enterCodeManually.tr(), + onTap: () => setState(() => isEnteringPasscode = true), + size: AFButtonSize.l, + alignment: Alignment.center, + ), + spacing, + ]; + } + + return [ + // Enter code manually + SizedBox( + height: 40, + child: AFTextField( + controller: passcodeController, + hintText: LocaleKeys.signIn_enterCode.tr(), + keyboardType: TextInputType.number, + radius: 10, + autoFocus: true, + onSubmitted: widget.onEnterPasscode, + ), + ), + // todo: ask designer to provide the spacing + VSpace(12), + + // continue to login + AFFilledTextButton.primary( + text: 'Continue to sign in', + onTap: () => widget.onEnterPasscode(passcodeController.text), + size: AFButtonSize.l, + alignment: Alignment.center, + ), + + spacing, + ]; + } + + List _buildBackToLogin() { + return [ + AFGhostTextButton( + text: LocaleKeys.signIn_backToLogin.tr(), + size: AFButtonSize.s, + onTap: widget.backToLogin, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.textColorScheme.theme; + }, + ), + ]; + } + + List _buildLogoTitleAndDescription() { + final theme = AppFlowyTheme.of(context); + final spacing = VSpace(theme.spacing.xxl); + return [ + // logo + const AFLogo(), + spacing, + + // title + Text( + LocaleKeys.signIn_checkYourEmail.tr(), + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), + ), + spacing, + + // description + Text( + LocaleKeys.signIn_temporaryVerificationSent.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + Text( + widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + spacing, + ]; + } + + void _showLoadingDialog() { + _dismissLoadingDialog(); + + toastificationItem = showToastNotification( + message: LocaleKeys.signIn_signingIn.tr(), + ); + } + + void _dismissLoadingDialog() { + final toastificationItem = this.toastificationItem; + if (toastificationItem != null) { + toastification.dismiss(toastificationItem); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart new file mode 100644 index 0000000000..5bfd191e22 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart @@ -0,0 +1,21 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class ContinueWithPassword extends StatelessWidget { + const ContinueWithPassword({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return AFOutlinedTextButton.normal( + text: 'Continue with password', + size: AFButtonSize.l, + alignment: Alignment.center, + onTap: onTap, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart new file mode 100644 index 0000000000..3a281889ad --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart @@ -0,0 +1,147 @@ +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +class ContinueWithPasswordPage extends StatefulWidget { + const ContinueWithPasswordPage({ + super.key, + required this.backToLogin, + required this.email, + required this.onEnterPassword, + required this.onForgotPassword, + }); + + final String email; + final VoidCallback backToLogin; + final ValueChanged onEnterPassword; + final VoidCallback onForgotPassword; + + @override + State createState() => + _ContinueWithPasswordPageState(); +} + +class _ContinueWithPasswordPageState extends State { + final passwordController = TextEditingController(); + + @override + void dispose() { + passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: SizedBox( + width: 320, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo and title + ..._buildLogoAndTitle(), + + // Password input and buttons + ..._buildPasswordSection(), + + // Back to login + ..._buildBackToLogin(), + ], + ), + ), + ), + ); + } + + List _buildLogoAndTitle() { + final theme = AppFlowyTheme.of(context); + final spacing = VSpace(theme.spacing.xxl); + return [ + // logo + const AFLogo(), + spacing, + + // title + Text( + 'Enter password', + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), + ), + spacing, + + // email display + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Login as ', + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + ), + TextSpan( + text: widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + ], + ), + ), + spacing, + ]; + } + + List _buildPasswordSection() { + return [ + // Password input + AFTextField( + controller: passwordController, + hintText: 'Enter password', + autoFocus: true, + onSubmitted: widget.onEnterPassword, + ), + // todo: ask designer to provide the spacing + VSpace(12), + + // todo: forgot password is not implemented yet + // Forgot password button + // AFGhostTextButton( + // text: 'Forget password?', + // size: AFButtonSize.s, + // onTap: widget.onForgotPassword, + // textColor: (context, isHovering, disabled) { + // return theme.textColorScheme.theme; + // }, + // ), + VSpace(12), + + // Continue button + AFFilledTextButton.primary( + text: 'Continue', + onTap: () => widget.onEnterPassword(passwordController.text), + size: AFButtonSize.l, + alignment: Alignment.center, + ), + VSpace(20), + ]; + } + + List _buildBackToLogin() { + return [ + AFGhostTextButton( + text: 'Back to Login', + size: AFButtonSize.s, + onTap: widget.backToLogin, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + return theme.textColorScheme.theme; + }, + ), + ]; + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart new file mode 100644 index 0000000000..eb29807b5b --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart @@ -0,0 +1,20 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + +class AFLogo extends StatelessWidget { + const AFLogo({ + super.key, + this.size = const Size.square(36), + }); + + final Size size; + + @override + Widget build(BuildContext context) { + return FlowySvg( + FlowySvgs.flowy_logo_xl, + blendMode: null, + size: size, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 69c9689823..45e4fe7273 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -64,14 +64,16 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { - return showToastNotification( - + showToastNotification( message: LocaleKeys.signIn_invalidEmail.tr(), type: ToastificationType.error, ); + return; } - context.read().add(SignInEvent.signedWithMagicLink(email)); + context + .read() + .add(SignInEvent.signInWithMagicLink(email: email)); showConfirmDialog( context: context, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index a5e0d9784d..4ea819d997 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -1,6 +1,7 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -12,41 +13,40 @@ class SignInAgreement extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + final textStyle = theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, + ); + final underlinedTextStyle = theme.textStyle.caption.underline( + color: theme.textColorScheme.secondary, + ); return RichText( textAlign: TextAlign.center, text: TextSpan( children: [ TextSpan( text: isLocalAuthEnabled - ? '${LocaleKeys.web_signInLocalAgreement.tr()} ' - : '${LocaleKeys.web_signInAgreement.tr()} ', - style: const TextStyle(color: Colors.grey, fontSize: 12), + ? '${LocaleKeys.web_signInLocalAgreement.tr()} \n' + : '${LocaleKeys.web_signInAgreement.tr()} \n', + style: textStyle, ), TextSpan( text: '${LocaleKeys.web_termOfUse.tr()} ', - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - decoration: TextDecoration.underline, - ), + style: underlinedTextStyle, mouseCursor: SystemMouseCursors.click, recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.io/terms'), + ..onTap = () => afLaunchUrlString('https://appflowy.com/terms'), ), TextSpan( text: '${LocaleKeys.web_and.tr()} ', - style: const TextStyle(color: Colors.grey, fontSize: 12), + style: textStyle, ), TextSpan( text: LocaleKeys.web_privacyPolicy.tr(), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - decoration: TextDecoration.underline, - ), + style: underlinedTextStyle, mouseCursor: SystemMouseCursors.click, recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'), + ..onTap = () => afLaunchUrlString('https://appflowy.com/privacy'), ), ], ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index 7fe4584e97..a0b31c2fe5 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -1,91 +1,14 @@ import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// Used in DesktopSignInScreen and MobileSignInScreen -class SignInAnonymousButton extends StatelessWidget { - const SignInAnonymousButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final isMobile = UniversalPlatform.isMobile; - - return BlocBuilder( - builder: (context, signInState) { - return BlocProvider( - create: (context) => AnonUserBloc() - ..add( - const AnonUserEvent.initial(), - ), - child: BlocListener( - listener: (context, state) async { - if (state.openedAnonUser != null) { - await runAppFlowy(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final text = state.anonUsers.isEmpty - ? LocaleKeys.signIn_loginStartWithAnonymous.tr() - : LocaleKeys.signIn_continueAnonymousUser.tr(); - final onTap = state.anonUsers.isEmpty - ? () { - context - .read() - .add(const SignInEvent.signedInAsGuest()); - } - : () { - final bloc = context.read(); - final user = bloc.state.anonUsers.first; - bloc.add(AnonUserEvent.openAnonUser(user)); - }; - // SignInAnonymousButton in mobile - if (isMobile) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - ), - onPressed: onTap, - child: FlowyText( - LocaleKeys.signIn_loginStartWithAnonymous.tr(), - fontSize: 14, - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: FontWeight.w500, - ), - ); - } - // SignInAnonymousButton in desktop - return SizedBox( - height: 48, - child: FlowyButton( - isSelected: true, - disable: signInState.isSubmitting, - text: FlowyText.medium( - text, - textAlign: TextAlign.center, - ), - radius: Corners.s6Border, - onTap: onTap, - ), - ); - }, - ), - ), - ); - }, - ); - } -} class SignInAnonymousButtonV2 extends StatelessWidget { const SignInAnonymousButtonV2({ @@ -109,27 +32,35 @@ class SignInAnonymousButtonV2 extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - final text = LocaleKeys.signIn_anonymous.tr(); + final theme = AppFlowyTheme.of(context); final onTap = state.anonUsers.isEmpty ? () { context .read() - .add(const SignInEvent.signedInAsGuest()); + .add(const SignInEvent.signInAsGuest()); } : () { final bloc = context.read(); final user = bloc.state.anonUsers.first; bloc.add(AnonUserEvent.openAnonUser(user)); }; - return FlowyButton( - useIntrinsicWidth: true, - onTap: onTap, - text: FlowyText( - text, - color: Colors.grey, - decoration: TextDecoration.underline, - fontSize: 12, + return AFGhostIconTextButton( + text: LocaleKeys.signIn_anonymousMode.tr(), + textColor: (context, isHovering, disabled) { + return theme.textColorScheme.secondary; + }, + padding: EdgeInsets.symmetric( + horizontal: theme.spacing.m, + vertical: theme.spacing.xs, ), + size: AFButtonSize.s, + onTap: onTap, + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.anonymous_mode_m, + color: theme.textColorScheme.secondary, + ); + }, ); }, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart index 5146e29962..7067844500 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; class MobileLogoutButton extends StatelessWidget { @@ -18,50 +18,19 @@ class MobileLogoutButton extends StatelessWidget { @override Widget build(BuildContext context) { - final style = Theme.of(context); - return GestureDetector( + return AFOutlinedIconTextButton.normal( + text: text, onTap: onPressed, - child: Container( - height: 38, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(4), - ), - border: Border.all( - color: textColor ?? style.colorScheme.outline, - width: 0.5, - ), - ), - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (icon != null) ...[ - SizedBox( - // The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space. - width: 30, - height: 30, - child: Center( - child: SizedBox( - width: 24, - child: FlowySvg( - icon!, - blendMode: null, - ), - ), - ), - ), - const HSpace(8), - ], - FlowyText( - text, - fontSize: 14.0, - fontWeight: FontWeight.w400, - color: textColor, - ), - ], - ), - ), + size: AFButtonSize.l, + iconBuilder: (context, isHovering, disabled) { + if (icon == null) { + return const SizedBox.shrink(); + } + return FlowySvg( + icon!, + size: Size.square(18), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart similarity index 53% rename from frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart rename to frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart index 35d16b031f..9a7234ab6b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart @@ -1,10 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; -import 'package:appflowy/user/presentation/widgets/widgets.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; enum ThirdPartySignInButtonType { @@ -102,118 +99,55 @@ class MobileThirdPartySignInButton extends StatelessWidget { super.key, this.height = 38, this.fontSize = 14.0, - required this.onPressed, + required this.onTap, required this.type, }); - final VoidCallback onPressed; + final VoidCallback onTap; final double height; final double fontSize; final ThirdPartySignInButtonType type; @override Widget build(BuildContext context) { - final style = Theme.of(context); - - return AnimatedGestureDetector( - scaleFactor: 1.0, - onTapUp: onPressed, - child: Container( - height: height, - decoration: BoxDecoration( - color: type.backgroundColor(context), - borderRadius: const BorderRadius.all( - Radius.circular(4), - ), - border: Border.all( - color: style.colorScheme.outline, - width: 0.5, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (type != ThirdPartySignInButtonType.anonymous) - FlowySvg( - type.icon, - size: Size.square(fontSize), - blendMode: type.blendMode, - color: type.textColor(context), - ), - const HSpace(8.0), - FlowyText( - type.labelText, - fontSize: fontSize, - color: type.textColor(context), - ), - ], - ), - ), + return AFOutlinedIconTextButton.normal( + text: type.labelText, + onTap: onTap, + size: AFButtonSize.l, + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + type.icon, + size: Size.square(16), + blendMode: type.blendMode, + ); + }, ); } } -class DesktopSignInButton extends StatelessWidget { - const DesktopSignInButton({ +class DesktopThirdPartySignInButton extends StatelessWidget { + const DesktopThirdPartySignInButton({ super.key, required this.type, - required this.onPressed, + required this.onTap, }); final ThirdPartySignInButtonType type; - final VoidCallback onPressed; + final VoidCallback onTap; @override Widget build(BuildContext context) { - final style = Theme.of(context); - // In desktop, the width of button is limited by [AuthFormContainer] - return SizedBox( - height: 48, - width: AuthFormContainer.width, - child: OutlinedButton.icon( - // In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left. - icon: Container( - width: AuthFormContainer.width / 4, - alignment: Alignment.centerRight, - child: SizedBox( - // Some icons are not square, so we just use a fixed width here. - width: 24, - child: FlowySvg( - type.icon, - blendMode: type.blendMode, - ), - ), - ), - label: Container( - padding: const EdgeInsets.only(left: 8), - alignment: Alignment.centerLeft, - child: FlowyText( - type.labelText, - fontSize: 14, - ), - ), - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.hovered)) { - return style.colorScheme.onSecondaryContainer; - } - return null; - }, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder( - borderRadius: Corners.s6Border, - ), - ), - side: WidgetStateProperty.all( - BorderSide( - color: style.dividerColor, - ), - ), - ), - onPressed: onPressed, - ), + return AFOutlinedIconTextButton.normal( + text: type.labelText, + onTap: onTap, + size: AFButtonSize.l, + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + type.icon, + size: Size.square(18), + blendMode: type.blendMode, + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart similarity index 68% rename from frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart rename to frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart index 7baa243e5f..ed2e060c49 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart @@ -1,8 +1,7 @@ import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -40,7 +39,7 @@ class ThirdPartySignInButtons extends StatelessWidget { void _signIn(BuildContext context, String provider) { context.read().add( - SignInEvent.signedInWithOAuth(provider), + SignInEvent.signInWithOAuth(platform: provider), ); } } @@ -58,23 +57,22 @@ class _DesktopThirdPartySignIn extends StatefulWidget { } class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { - static const padding = 12.0; - bool isExpanded = false; @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( children: [ - DesktopSignInButton( + DesktopThirdPartySignInButton( key: signInWithGoogleButtonKey, type: ThirdPartySignInButtonType.google, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), ), - const VSpace(padding), - DesktopSignInButton( + VSpace(theme.spacing.l), + DesktopThirdPartySignInButton( type: ThirdPartySignInButtonType.apple, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), ), ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), ], @@ -82,38 +80,38 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { } List _buildExpandedButtons() { + final theme = AppFlowyTheme.of(context); return [ - const VSpace(padding * 1.5), - DesktopSignInButton( + VSpace(theme.spacing.l), + DesktopThirdPartySignInButton( type: ThirdPartySignInButtonType.github, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), ), - const VSpace(padding), - DesktopSignInButton( + VSpace(theme.spacing.l), + DesktopThirdPartySignInButton( type: ThirdPartySignInButtonType.discord, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), ), ]; } List _buildCollapsedButtons() { + final theme = AppFlowyTheme.of(context); return [ - const VSpace(padding), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, - child: FlowyText( - LocaleKeys.signIn_continueAnotherWay.tr(), - color: Theme.of(context).colorScheme.onSurface, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), + VSpace(theme.spacing.l), + AFGhostTextButton( + text: 'More options', + textColor: (context, isHovering, disabled) { + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.textColorScheme.theme; + }, + onTap: () { + setState(() { + isExpanded = !isExpanded; + }); + }, ), ]; } @@ -153,14 +151,14 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { if (Platform.isIOS) ...[ MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.apple, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), ), const VSpace(padding), ], MobileThirdPartySignInButton( key: signInWithGoogleButtonKey, type: ThirdPartySignInButtonType.google, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), ), ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), ], @@ -172,31 +170,33 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { const VSpace(padding), MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.github, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), ), const VSpace(padding), MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.discord, - onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), ), ]; } List _buildCollapsedButtons() { + final theme = AppFlowyTheme.of(context); return [ const VSpace(padding * 2), - GestureDetector( + AFGhostTextButton( + text: 'More options', + textColor: (context, isHovering, disabled) { + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.textColorScheme.theme; + }, onTap: () { setState(() { isExpanded = !isExpanded; }); }, - child: FlowyText( - LocaleKeys.signIn_continueAnotherWay.tr(), - color: Theme.of(context).colorScheme.onSurface, - decoration: TextDecoration.underline, - fontSize: 14, - ), ), ]; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 18e260a472..6d79b896c1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,7 +1,7 @@ -export 'magic_link_sign_in_buttons.dart'; +export 'continue_with/continue_with_email_and_password.dart'; +export 'sign_in_agreement.dart'; export 'sign_in_anonymous_button.dart'; export 'sign_in_or_logout_button.dart'; -export 'third_party_sign_in_button.dart'; +export 'third_party_sign_in_button/third_party_sign_in_button.dart'; // export 'switch_sign_in_sign_up_button.dart'; -export 'third_party_sign_in_buttons.dart'; -export 'sign_in_agreement.dart'; +export 'third_party_sign_in_button/third_party_sign_in_buttons.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart index 8ce09a5b7f..c0b8e7e5ae 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart @@ -8,7 +8,7 @@ class AuthFormContainer extends StatelessWidget { final List children; - static const double width = 340; + static const double width = 320; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart index c2a13eac82..93ccea25d0 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart @@ -1,8 +1,7 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/size.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; class FlowyLogoTitle extends StatelessWidget { const FlowyLogoTitle({ @@ -16,24 +15,19 @@ class FlowyLogoTitle extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return SizedBox( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox.fromSize( - size: logoSize, - child: const FlowySvg( - FlowySvgs.flowy_logo_xl, - blendMode: null, - ), - ), + AFLogo(size: logoSize), const VSpace(20), - FlowyText.regular( + Text( title, - fontSize: FontSizes.s24, - fontFamily: - GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily, - color: Theme.of(context).colorScheme.tertiary, + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), ), ], ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart index 1737494530..2fae0e4872 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart @@ -188,6 +188,9 @@ class SidebarPlanBloc extends Bloc { UserEventGetWorkspaceUsage(payload).send().then((result) { result.onSuccess( (usage) { + if (isClosed) { + return; + } add(SidebarPlanEvent.updateWorkspaceUsage(usage)); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 13f5d832d0..984598f29c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -4,7 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/prelude.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart'; import 'package:appflowy/util/navigator_context_extension.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -111,7 +111,7 @@ class _SignInDialogContent extends StatelessWidget { const _DialogHeader(), const _DialogTitle(), const VSpace(16), - const SignInWithMagicLinkButtons(), + const ContinueWithEmailAndPassword(), if (isAuthEnabled) ...[ const VSpace(20), const _OrDivider(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 3bcc840582..7e30c4fa55 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -362,7 +362,7 @@ class OkCancelButton extends StatelessWidget { } } -void showToastNotification({ +ToastificationItem showToastNotification({ String? message, TextSpan? richMessage, String? description, @@ -374,7 +374,7 @@ void showToastNotification({ (message == null) != (richMessage == null), "Exactly one of message or richMessage must be non-null.", ); - toastification.showCustom( + return toastification.showCustom( alignment: Alignment.bottomCenter, autoCloseDuration: const Duration(milliseconds: 3000), callbacks: callbacks ?? const ToastificationCallbacks(), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/README.md index 75218b1842..953d3545f1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/README.md +++ b/frontend/appflowy_flutter/packages/appflowy_ui/README.md @@ -9,7 +9,7 @@ AppFlowy UI is a Flutter package that provides a collection of reusable UI compo ## Installation -Add the following to your *app's* `pubspec.yaml` file: +Add the following to your `pubspec.yaml` file: ```yaml dependencies: diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart index 92d4484c0c..974907f940 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart @@ -1,4 +1,2 @@ -library; - export 'src/component/component.dart'; export 'src/theme/theme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 668644f196..d48c061c71 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -206,7 +206,7 @@ class AppFlowyThemeBuilder { quaternary: colorScheme.neutral.neutral100, quaternaryHover: colorScheme.neutral.neutral200, transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey100005, + primaryAlpha5: colorScheme.neutral.alphaGrey10005, primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010, primaryAlpha80: colorScheme.neutral.alphaGrey100080, primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index fecbc32b89..1c6d66431d 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -126,6 +126,13 @@ packages: relative: true source: path version: "0.0.1" + appflowy_ui: + dependency: "direct main" + description: + path: "packages/appflowy_ui" + relative: true + source: path + version: "1.0.0" archive: dependency: "direct main" description: @@ -2597,5 +2604,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.6.2 <4.0.0" flutter: ">=3.27.4" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 9b0987d0b2..0cc553d1a7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -25,7 +25,8 @@ dependencies: path: packages/appflowy_popover appflowy_result: path: packages/appflowy_result - + appflowy_ui: + path: packages/appflowy_ui archive: ^3.4.10 auto_size_text_field: ^2.2.3 auto_updater: ^1.0.0 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 43a59974fd..e42660436c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -37,6 +37,7 @@ "loginStartWithAnonymous": "Continue with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", "anonymous": "Anonymous", + "anonymousMode": "Anonymous mode", "buttonText": "Sign In", "signingInText": "Signing in...", "forgotPassword": "Forgot Password?", @@ -68,7 +69,16 @@ "logIn": "Log in", "generalError": "Something went wrong. Please try again later", "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", - "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes." + "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.", + "tokenHasExpiredOrInvalid": "The token has expired or is invalid. Please try again.", + "signingIn": "Signing in...", + "checkYourEmail": "Check your email", + "temporaryVerificationSent": "A temporary verification link has been sent. Please check your inbox at", + "continueToSignIn": "Continue to sign in", + "backToLogin": "Back to login", + "enterCode": "Enter code", + "enterCodeManually": "Enter code manually", + "continueWithEmail": "Continue with email" }, "workspace": { "chooseWorkspace": "Choose your workspace", diff --git a/frontend/rust-lib/.vscode/launch.json b/frontend/rust-lib/.vscode/launch.json new file mode 100644 index 0000000000..3b1e6e62a7 --- /dev/null +++ b/frontend/rust-lib/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "AF-desktop: Debug Rust", + "type": "lldb", + // "request": "attach", + // "pid": "${command:pickMyProcess}" + // To launch the application directly, use the following configuration: + "request": "launch", + "program": "/Users/lucas.xu/Desktop/appflowy_backup/frontend/appflowy_flutter/build/macos/Build/Products/Debug/AppFlowy.app", + }, + ] + } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a251594ef9..fdf8c8348e 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "bytes", @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "again", "anyhow", @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1202,7 +1202,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "futures-channel", "futures-util", @@ -1474,7 +1474,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "bincode", @@ -1496,7 +1496,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "async-trait", @@ -1944,7 +1944,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "bincode", "bytes", @@ -3418,7 +3418,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3433,7 +3433,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "app-error", "jsonwebtoken", @@ -4054,7 +4054,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "bytes", @@ -6754,7 +6754,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 4df29d1fe4..d3427ef99c 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 6b2917cc62..3c79cd38ca 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -14,7 +14,7 @@ use client_api::entity::workspace_dto::{ }; use client_api::entity::{ AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, - AuthProvider, CollabParams, CreateCollabParams, QueryWorkspaceMember, + AuthProvider, CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; @@ -121,16 +121,13 @@ where &self, email: &str, password: &str, - ) -> Result { + ) -> Result { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); let client = try_get_client?; - client.sign_in_password(&email, &password).await?; - let profile = client.get_profile().await?; - let token = client.get_token()?; - let profile = user_profile_from_af_profile(token, profile)?; - Ok(profile) + let response = client.sign_in_password(&email, &password).await?; + Ok(response) } async fn sign_in_with_magic_link( @@ -148,6 +145,19 @@ where Ok(()) } + async fn sign_in_with_passcode( + &self, + email: &str, + passcode: &str, + ) -> Result { + let email = email.to_owned(); + let passcode = passcode.to_owned(); + let try_get_client = self.server.try_get_client(); + let client = try_get_client?; + let response = client.sign_in_with_passcode(&email, &passcode).await?; + Ok(response) + } + async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result { let provider = AuthProvider::from(provider); let try_get_client = self.server.try_get_client(); diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 962c0506bf..8d9e342e85 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,4 +1,5 @@ #![allow(unused_variables)] +use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; @@ -98,7 +99,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { &self, _email: &str, _password: &str, - ) -> Result { + ) -> Result { Err(FlowyError::local_version_not_support().with_context("Not support")) } @@ -110,6 +111,14 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { Err(FlowyError::local_version_not_support().with_context("Not support")) } + async fn sign_in_with_passcode( + &self, + _email: &str, + _passcode: &str, + ) -> Result { + Err(FlowyError::local_version_not_support().with_context("Not support")) + } + async fn generate_oauth_url_with_provider(&self, _provider: &str) -> Result { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 65d02c704a..3712307af4 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -199,6 +199,16 @@ where }) } + fn sign_in_with_passcode( + &self, + _email: &str, + _passcode: &str, + ) -> FutureResult { + FutureResult::new(async { + Err(FlowyError::not_support().with_context("Can't sign in with passcode when using supabase")) + }) + } + fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult { FutureResult::new(async { Err(FlowyError::internal().with_context("Can't generate oauth url when using supabase")) diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index b549c549fc..3f7f39910b 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -4,6 +4,7 @@ use client_api::entity::billing_dto::SubscriptionPlanDetail; pub use client_api::entity::billing_dto::SubscriptionStatus; use client_api::entity::billing_dto::WorkspaceSubscriptionStatus; use client_api::entity::billing_dto::WorkspaceUsageAndLimit; +use client_api::entity::GotrueTokenResponse; pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use collab_entity::{CollabObject, CollabType}; use flowy_error::{internal_error, ErrorCode, FlowyError}; @@ -148,11 +149,17 @@ pub trait UserCloudService: Send + Sync + 'static { &self, email: &str, password: &str, - ) -> Result; + ) -> Result; async fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) -> Result<(), FlowyError>; + async fn sign_in_with_passcode( + &self, + email: &str, + passcode: &str, + ) -> Result; + /// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page. /// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.), /// which will call [Client::sign_in_with_url]generate_sign_in_url_with_email to sign in. diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index dbfd9b811a..02ae333aa0 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::convert::TryInto; +use client_api::entity::GotrueTokenResponse; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; @@ -86,6 +87,53 @@ pub struct MagicLinkSignInPB { pub redirect_to: String, } +#[derive(ProtoBuf, Default)] +pub struct PasscodeSignInPB { + #[pb(index = 1)] + pub email: String, + + #[pb(index = 2)] + pub passcode: String, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct GotrueTokenResponsePB { + #[pb(index = 1)] + pub access_token: String, + + #[pb(index = 2)] + pub token_type: String, + + #[pb(index = 3)] + pub expires_in: i64, + + #[pb(index = 4)] + pub expires_at: i64, + + #[pb(index = 5)] + pub refresh_token: String, + + #[pb(index = 6, one_of)] + pub provider_access_token: Option, + + #[pb(index = 7, one_of)] + pub provider_refresh_token: Option, +} + +impl From for GotrueTokenResponsePB { + fn from(response: GotrueTokenResponse) -> Self { + Self { + access_token: response.access_token, + token_type: response.token_type, + expires_in: response.expires_in, + expires_at: response.expires_at, + refresh_token: response.refresh_token, + provider_access_token: response.provider_access_token, + provider_refresh_token: response.provider_refresh_token, + } + } +} + #[derive(ProtoBuf, Default)] pub struct OauthSignInPB { /// Use this field to store the third party auth information. diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 4d2ee57ce8..0e42525d04 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -41,14 +41,16 @@ fn upgrade_store_preferences( pub async fn sign_in_with_email_password_handler( data: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let auth_type = params.auth_type.clone(); let old_authenticator = manager.cloud_services.get_user_authenticator(); - match manager.sign_in(params, auth_type).await { - Ok(profile) => data_result_ok(UserProfilePB::from(profile)), + match manager + .sign_in_with_password(¶ms.email, ¶ms.password) + .await + { + Ok(token) => data_result_ok(token.into()), Err(err) => { manager .cloud_services @@ -319,6 +321,19 @@ pub async fn sign_in_with_magic_link_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub async fn sign_in_with_passcode_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let params = data.into_inner(); + let response = manager + .sign_in_with_passcode(¶ms.email, ¶ms.passcode) + .await?; + data_result_ok(response.into()) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn oauth_sign_in_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 0807b46170..2de1fdfbdc 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -81,7 +81,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting) .event(UserEvent::GetWorkspaceSetting, get_workspace_setting) .event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler) - + .event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -89,7 +89,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { pub enum UserEvent { /// Only use when the [Authenticator] is Local or SelfHosted /// Logging into an account using a register email and password - #[event(input = "SignInPayloadPB", output = "UserProfilePB")] + #[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")] SignInWithEmailPassword = 0, /// Only use when the [Authenticator] is Local or SelfHosted @@ -278,6 +278,9 @@ pub enum UserEvent { #[event()] DeleteAccount = 64, + + #[event(input = "PasscodeSignInPB", output = "GotrueTokenResponsePB")] + PasscodeSignIn = 65, } #[async_trait] diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 864629d169..b4bc8911e1 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,3 +1,4 @@ +use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_error::{internal_error, ErrorCode, FlowyResult}; @@ -719,6 +720,19 @@ impl UserManager { Ok(url) } + pub(crate) async fn sign_in_with_password( + &self, + email: &str, + password: &str, + ) -> Result { + self + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; + let response = auth_service.sign_in_with_password(email, password).await?; + Ok(response) + } + pub(crate) async fn sign_in_with_magic_link( &self, email: &str, @@ -734,6 +748,19 @@ impl UserManager { Ok(()) } + pub(crate) async fn sign_in_with_passcode( + &self, + email: &str, + passcode: &str, + ) -> Result { + self + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; + let response = auth_service.sign_in_with_passcode(email, passcode).await?; + Ok(response) + } + pub(crate) async fn generate_oauth_url( &self, oauth_provider: &str, From 4997ac99cf4c6445fc730ab2d19fb8b952299728 Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 10 Apr 2025 14:45:13 +0800 Subject: [PATCH 299/384] feat: revamp link preivew (#7692) * feat: revamp link preivew * feat: add convert to menu for link hover menu * feat: add mention link * feat: support convert preview to mention * feat: add embed link preview * fix: some test erros * fix: test errors * fix: some UI issues * chore: add test for url * chore: add test for mention * chore: add test for bookmark * chore: add test for embed * chore: remove unuse import * fix: some UI issues * fix: remove text span overlay on mobile * fix: code lint error --------- Co-authored-by: Lucas --- .../document_copy_and_paste_test.dart | 44 +- .../document/document_link_preview_test.dart | 452 ++++++++++++++++++ .../document/document_test_runner_4.dart | 2 + .../uncategorized/emoji_shortcut_test.dart | 5 +- .../presentation/editor_configuration.dart | 19 +- .../copy_and_paste/custom_paste_command.dart | 5 + .../copy_and_paste/paste_from_html.dart | 2 + .../copy_and_paste/paste_from_plain_text.dart | 26 + .../link/link_create_menu.dart | 7 +- .../desktop_toolbar/link/link_hover_menu.dart | 160 ++++++- .../link/link_replace_menu.dart | 184 +++++++ .../link_embed_block_component.dart | 286 +++++++++++ .../link_embed/link_embed_menu.dart | 332 +++++++++++++ .../link_preview/custom_link_parser.dart | 135 ++++++ .../link_preview/custom_link_preview.dart | 116 +++-- .../custom_link_preview_block_component.dart | 204 ++++++++ .../link_preview/link_preview_menu.dart | 262 ++++++---- .../link_preview/paste_as/paste_as_menu.dart | 253 ++++++++++ .../editor_plugins/link_preview/shared.dart | 173 ++++++- .../editor_plugins/mention/mention_block.dart | 16 + .../mention/mention_link_block.dart | 337 +++++++++++++ .../mention/mention_link_error_preview.dart | 232 +++++++++ .../mention/mention_link_preview.dart | 271 +++++++++++ .../editor_plugins/menu/menu_extension.dart | 116 +++++ .../custom_hightlight_color_toolbar_item.dart | 3 + .../document/presentation/editor_style.dart | 5 +- .../lib/plugins/emoji/emoji_handler.dart | 3 +- .../lib/startup/tasks/app_widget.dart | 28 +- .../lib/util/default_extensions.dart | 8 + .../lib/src/theme/data/builder.dart | 4 +- .../flowy_infra/lib/theme_extension_v2.dart | 8 + .../lib/widget/flowy_tooltip.dart | 1 - frontend/appflowy_flutter/pubspec.lock | 8 + frontend/appflowy_flutter/pubspec.yaml | 1 + .../flowy_icons/20x/embed_fullscreen.svg | 3 + .../resources/flowy_icons/20x/turninto.svg | 3 + .../resources/flowy_icons/40x/embed_error.svg | 7 + frontend/resources/translations/en.json | 24 +- 38 files changed, 3571 insertions(+), 174 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart create mode 100644 frontend/resources/flowy_icons/20x/embed_fullscreen.svg create mode 100644 frontend/resources/flowy_icons/20x/turninto.svg create mode 100644 frontend/resources/flowy_icons/40x/embed_error.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index c18b42939c..ee82f01d3f 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -1,10 +1,12 @@ +import 'dart:async'; import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; @@ -320,8 +322,14 @@ void main() { (tester) async { const url = 'https://appflowy.io'; await tester.pasteContent(plainText: url, (editorState) async { + final pasteAsMenu = find.byType(PasteAsMenu); + expect(pasteAsMenu, findsOneWidget); + final bookmarkButton = find.text( + LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(), + ); + await tester.tapButton(bookmarkButton); // the second one is the paragraph node - expect(editorState.document.root.children.length, 2); + expect(editorState.document.root.children.length, 1); final node = editorState.getNodeAtPath([0])!; expect(node.type, LinkPreviewBlockKeys.type); expect(node.attributes[LinkPreviewBlockKeys.url], url); @@ -333,19 +341,19 @@ void main() { await tester.hoverOnWidget( find.byType(CustomLinkPreviewWidget), onHover: () async { - final convertToLinkButton = find.byWidgetPredicate((widget) { - return widget is MenuBlockButton && - widget.tooltip == - LocaleKeys.document_plugins_urlPreview_convertToLink.tr(); - }); + /// show menu + final menu = find.byType(CustomLinkPreviewMenu); + expect(menu, findsOneWidget); + await tester.tapButton(menu); + + final convertToLinkButton = find.text( + LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl.tr(), + ); expect(convertToLinkButton, findsOneWidget); - await tester.tap(convertToLinkButton); - await tester.pumpAndSettle(); + await tester.tapButton(convertToLinkButton); }, ); - await tester.pumpAndSettle(); - final editorState = tester.editor.getCurrentEditorState(); final textNode = editorState.getNodeAtPath([0])!; expect(textNode.type, ParagraphBlockKeys.type); @@ -363,14 +371,20 @@ void main() { (tester) async { const url = 'https://appflowy.io'; await tester.pasteContent(plainText: url, (editorState) async { + final pasteAsMenu = find.byType(PasteAsMenu); + expect(pasteAsMenu, findsOneWidget); + final bookmarkButton = find.text( + LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(), + ); + await tester.tapButton(bookmarkButton); // the second one is the paragraph node - expect(editorState.document.root.children.length, 2); + expect(editorState.document.root.children.length, 1); final node = editorState.getNodeAtPath([0])!; expect(node.type, LinkPreviewBlockKeys.type); expect(node.attributes[LinkPreviewBlockKeys.url], url); }); - await tester.editor.tapLineOfEditorAt(0); + await tester.simulateKeyEvent( LogicalKeyboardKey.keyZ, isControlPressed: @@ -521,7 +535,7 @@ void main() { extension on WidgetTester { Future pasteContent( - void Function(EditorState editorState) test, { + FutureOr Function(EditorState editorState) test, { Future Function(EditorState editorState)? beforeTest, String? plainText, String? html, @@ -558,6 +572,6 @@ extension on WidgetTester { ); await pumpAndSettle(const Duration(milliseconds: 1000)); - test(editor.getCurrentEditorState()); + await test(editor.getCurrentEditorState()); } } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart new file mode 100644 index 0000000000..f74011ee2b --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart @@ -0,0 +1,452 @@ +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/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +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 '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const avaliableLink = 'https://appflowy.io/', + unavailableLink = 'www.thereIsNoting.com'; + + Future preparePage(WidgetTester tester, {String? pageName}) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.createNewPageWithNameUnderParent(name: pageName); + await tester.editor.tapLineOfEditorAt(0); + } + + Future pasteLink(WidgetTester tester, String link) async { + await getIt() + .setData(ClipboardServiceData(plainText: link)); + + /// paste the link + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyV, + isControlPressed: Platform.isLinux || Platform.isWindows, + isMetaPressed: Platform.isMacOS, + ); + await tester.pumpAndSettle(Duration(seconds: 1)); + } + + Future pasteAs( + WidgetTester tester, + String link, + PasteMenuType type, { + Duration waitTime = const Duration(milliseconds: 500), + }) async { + await pasteLink(tester, link); + final convertToMentionButton = find.text(type.title); + await tester.tapButton(convertToMentionButton); + await tester.pumpAndSettle(waitTime); + } + + void checkUrl(Node node, String link) { + expect(node.type, ParagraphBlockKeys.type); + expect(node.delta!.toJson(), [ + { + 'insert': link, + 'attributes': {'href': link}, + } + ]); + } + + void checkMention(Node node, String link) { + final delta = node.delta!; + final insert = (delta.first as TextInsert).text; + final attributes = delta.first.attributes; + expect(insert, MentionBlockKeys.mentionChar); + final mention = + attributes?[MentionBlockKeys.mention] as Map; + expect(mention[MentionBlockKeys.type], MentionType.externalLink.name); + expect(mention[MentionBlockKeys.url], avaliableLink); + } + + void checkBookmark(Node node, String link) { + expect(node.type, LinkPreviewBlockKeys.type); + expect(node.attributes[LinkPreviewBlockKeys.url], link); + } + + void checkEmbed(Node node, String link) { + expect(node.type, LinkPreviewBlockKeys.type); + expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed); + expect(node.attributes[LinkPreviewBlockKeys.url], link); + } + + group('Paste as URL', () { + Future pasteAndTurnInto( + WidgetTester tester, + String link, + String title, + ) async { + await pasteLink(tester, link); + final convertToLinkButton = find + .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr()); + await tester.tapButton(convertToLinkButton); + + /// hover link and turn into mention + await tester.hoverOnWidget( + find.byType(LinkHoverTrigger), + onHover: () async { + final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m); + await tester.tapButton(turnintoButton); + final convertToButton = find.text(title); + await tester.tapButton(convertToButton); + await tester.pumpAndSettle(Duration(seconds: 1)); + }, + ); + } + + testWidgets('paste a link', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteLink(tester, link); + final convertToLinkButton = find + .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr()); + await tester.tapButton(convertToLinkButton); + final node = tester.editor.getNodeAtPath([0]); + checkUrl(node, link); + }); + + testWidgets('paste a link and turn into mention', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAndTurnInto( + tester, + link, + LinkConvertMenuCommand.toMention.title, + ); + + /// check metion values + final node = tester.editor.getNodeAtPath([0]); + checkMention(node, link); + }); + + testWidgets('paste a link and turn into bookmark', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAndTurnInto( + tester, + link, + LinkConvertMenuCommand.toBookmark.title, + ); + + /// check metion values + final node = tester.editor.getNodeAtPath([0]); + checkBookmark(node, link); + }); + + testWidgets('paste a link and turn into embed', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAndTurnInto( + tester, + link, + LinkConvertMenuCommand.toEmbed.title, + ); + + /// check metion values + final node = tester.editor.getNodeAtPath([0]); + checkEmbed(node, link); + }); + }); + + group('Paste as Mention', () { + Future pasteAsMention(WidgetTester tester, String link) => + pasteAs(tester, link, PasteMenuType.mention); + + String getMentionLink(Node node) { + final insert = node.delta?.first as TextInsert?; + final mention = insert?.attributes?[MentionBlockKeys.mention] + as Map?; + return mention?[MentionBlockKeys.url] ?? ''; + } + + Future hoverMentionAndClick( + WidgetTester tester, + String command, + ) async { + final mentionLink = find.byType(MentionLinkBlock); + expect(mentionLink, findsOneWidget); + await tester.hoverOnWidget( + mentionLink, + onHover: () async { + final errorPreview = find.byType(MentionLinkErrorPreview); + expect(errorPreview, findsOneWidget); + final convertButton = find.byFlowySvg(FlowySvgs.turninto_m); + await tester.tapButton(convertButton); + final menuButton = find.text(command); + await tester.tapButton(menuButton); + }, + ); + } + + testWidgets('paste a link as mention', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + final node = tester.editor.getNodeAtPath([0]); + checkMention(node, link); + }); + + testWidgets('paste as mention and copy link', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + final mentionLink = find.byType(MentionLinkBlock); + expect(mentionLink, findsOneWidget); + await tester.hoverOnWidget( + mentionLink, + onHover: () async { + final preview = find.byType(MentionLinkPreview); + if (!preview.hasFound) { + final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); + await tester.tapButton(copyButton); + } else { + final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m); + await tester.tapButton(moreOptionButton); + final copyButton = + find.text(MentionLinktMenuCommand.copyLink.title); + await tester.tapButton(copyButton); + } + }, + ); + final clipboardContent = await getIt().getData(); + expect(clipboardContent.plainText, link); + }); + + testWidgets('paste as error mention and turninto url', (tester) async { + String link = unavailableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + Node node = tester.editor.getNodeAtPath([0]); + link = getMentionLink(node); + await hoverMentionAndClick( + tester, + MentionLinktErrorMenuCommand.toURL.title, + ); + node = tester.editor.getNodeAtPath([0]); + checkUrl(node, link); + }); + + testWidgets('paste as error mention and turninto embed', (tester) async { + String link = unavailableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + Node node = tester.editor.getNodeAtPath([0]); + link = getMentionLink(node); + await hoverMentionAndClick( + tester, + MentionLinktErrorMenuCommand.toEmbed.title, + ); + node = tester.editor.getNodeAtPath([0]); + checkEmbed(node, link); + }); + + testWidgets('paste as error mention and turninto bookmark', (tester) async { + String link = unavailableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + Node node = tester.editor.getNodeAtPath([0]); + link = getMentionLink(node); + await hoverMentionAndClick( + tester, + MentionLinktErrorMenuCommand.toBookmark.title, + ); + node = tester.editor.getNodeAtPath([0]); + checkBookmark(node, link); + }); + + testWidgets('paste as error mention and remove link', (tester) async { + String link = unavailableLink; + await preparePage(tester); + await pasteAsMention(tester, link); + Node node = tester.editor.getNodeAtPath([0]); + link = getMentionLink(node); + await hoverMentionAndClick( + tester, + MentionLinktErrorMenuCommand.removeLink.title, + ); + node = tester.editor.getNodeAtPath([0]); + expect(node.type, ParagraphBlockKeys.type); + expect(node.delta!.toJson(), [ + {'insert': link}, + ]); + }); + }); + + group('Paste as Bookmark', () { + Future pasteAsBookmark(WidgetTester tester, String link) => + pasteAs(tester, link, PasteMenuType.bookmark); + + Future hoverAndClick( + WidgetTester tester, + LinkPreviewMenuCommand command, + ) async { + final bookmark = find.byType(CustomLinkPreviewBlockComponent); + expect(bookmark, findsOneWidget); + await tester.hoverOnWidget( + bookmark, + onHover: () async { + final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m); + await tester.tapButton(menuButton); + final commandButton = find.text(command.title); + await tester.tapButton(commandButton); + }, + ); + } + + testWidgets('paste a link as bookmark', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + final node = tester.editor.getNodeAtPath([0]); + checkBookmark(node, link); + }); + + testWidgets('paste a link as bookmark and convert to mention', + (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention); + final node = tester.editor.getNodeAtPath([0]); + checkMention(node, link); + }); + + testWidgets('paste a link as bookmark and convert to url', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl); + final node = tester.editor.getNodeAtPath([0]); + checkUrl(node, link); + }); + + testWidgets('paste a link as bookmark and convert to embed', + (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed); + final node = tester.editor.getNodeAtPath([0]); + checkEmbed(node, link); + }); + + testWidgets('paste a link as bookmark and copy link', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink); + final clipboardContent = await getIt().getData(); + expect(clipboardContent.plainText, link); + }); + + testWidgets('paste a link as bookmark and replace link', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.replace); + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyA, + isControlPressed: Platform.isLinux || Platform.isWindows, + isMetaPressed: Platform.isMacOS, + ); + await tester.simulateKeyEvent(LogicalKeyboardKey.delete); + await tester.enterText(find.byType(TextFormField), unavailableLink); + await tester.tapButton(find.text(LocaleKeys.button_replace.tr())); + final node = tester.editor.getNodeAtPath([0]); + checkBookmark(node, unavailableLink); + }); + + testWidgets('paste a link as bookmark and remove link', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsBookmark(tester, link); + await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink); + final node = tester.editor.getNodeAtPath([0]); + expect(node.type, ParagraphBlockKeys.type); + expect(node.delta!.toJson(), [ + {'insert': link}, + ]); + }); + }); + group('Paste as Embed', () { + Future pasteAsEmbed(WidgetTester tester, String link) => + pasteAs(tester, link, PasteMenuType.embed); + + Future hoverAndConvert( + WidgetTester tester, + PasteMenuType command, + ) async { + final embed = find.byType(LinkEmbedBlockComponent); + expect(embed, findsOneWidget); + await tester.hoverOnWidget( + embed, + onHover: () async { + final menuButton = find.byFlowySvg(FlowySvgs.turninto_m); + await tester.tapButton(menuButton); + final commandButton = find.text(command.title); + await tester.tapButton(commandButton); + }, + ); + } + + testWidgets('paste a link as embed', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsEmbed(tester, link); + final node = tester.editor.getNodeAtPath([0]); + checkEmbed(node, link); + }); + + testWidgets('paste a link as bookmark and convert to mention', + (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsEmbed(tester, link); + await hoverAndConvert(tester, PasteMenuType.mention); + final node = tester.editor.getNodeAtPath([0]); + checkMention(node, link); + }); + + testWidgets('paste a link as bookmark and convert to url', (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsEmbed(tester, link); + await hoverAndConvert(tester, PasteMenuType.url); + final node = tester.editor.getNodeAtPath([0]); + checkUrl(node, link); + }); + + testWidgets('paste a link as bookmark and convert to bookmark', + (tester) async { + final link = avaliableLink; + await preparePage(tester); + await pasteAsEmbed(tester, link); + await hoverAndConvert(tester, PasteMenuType.bookmark); + final node = tester.editor.getNodeAtPath([0]); + checkBookmark(node, link); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart index a05545753e..bc0671834b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart @@ -13,6 +13,7 @@ import 'document_with_multi_image_block_test.dart' as document_with_multi_image_block_test; import 'document_with_simple_table_test.dart' as document_with_simple_table_test; +import 'document_link_preview_test.dart' as document_link_preview_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -28,4 +29,5 @@ void main() { document_find_menu_test.main(); document_toolbar_test.main(); document_with_simple_table_test.main(); + document_link_preview_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index aba3a7be06..1a4e57078f 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -43,7 +43,10 @@ void main() { }); group('insert emoji by colon', () { - Future createNewDocumentAndShowEmojiList(WidgetTester tester, {String? search}) async { + Future createNewDocumentAndShowEmojiList( + WidgetTester tester, { + String? search, + }) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); await tester.createNewPageWithNameUnderParent(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 67ab383eba..b3d99ab84b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'editor_plugins/link_preview/custom_link_preview_block_component.dart'; import 'editor_plugins/page_block/custom_page_block_component.dart'; /// A global configuration for the editor. @@ -969,11 +970,11 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder( ); } -LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( +CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( BuildContext context, BlockComponentConfiguration configuration, ) { - return LinkPreviewBlockComponentBuilder( + return CustomLinkPreviewBlockComponentBuilder( configuration: configuration.copyWith( padding: (node) { if (UniversalPlatform.isMobile) { @@ -983,20 +984,6 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( }, ), cache: LinkPreviewDataCache(), - showMenu: true, - menuBuilder: (context, node, state) => Positioned( - top: 10, - right: 0, - child: LinkPreviewMenu(node: node, state: state), - ), - builder: (_, node, url, title, description, imageUrl) => - CustomLinkPreviewWidget( - node: node, - url: url, - title: title, - description: description, - imageUrl: imageUrl, - ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 5c25d470d5..dd3f7362a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:string_validator/string_validator.dart'; +import 'package:universal_platform/universal_platform.dart'; /// - support /// - desktop @@ -162,6 +163,7 @@ Future _pasteAsLinkPreview( EditorState editorState, String? text, ) async { + final isMobile = UniversalPlatform.isMobile; // the url should contain a protocol if (text == null || !isURL(text, {'require_protocol': true})) { return false; @@ -193,6 +195,8 @@ Future _pasteAsLinkPreview( return false; } + if (!isMobile && !isImageUrl) return false; + // insert the text with link format final textTransaction = editorState.transaction ..insertText( @@ -250,6 +254,7 @@ Future doPlainPaste(EditorState editorState) async { } Future _isImageUrl(String text) async { + if (isNotImageUrl(text)) return false; final response = await http.head(Uri.parse(text)); if (response.statusCode == 200) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart index 6eff666991..3f11759545 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; @@ -13,6 +14,7 @@ extension PasteFromHtml on EditorState { } if (nodes.length == 1) { await pasteSingleLineNode(nodes.first); + checkToShowPasteAsMenu(nodes.first); } else { await pasteMultiLineNodes(nodes.toList()); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart index 4728eb7cf6..fcb12cefa5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart @@ -1,6 +1,8 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:universal_platform/universal_platform.dart'; extension PasteFromPlainText on EditorState { Future pastePlainText(String plainText) async { @@ -41,6 +43,7 @@ extension PasteFromPlainText on EditorState { } if (nodes.length == 1) { await pasteSingleLineNode(nodes.first); + checkToShowPasteAsMenu(nodes.first); } else { await pasteMultiLineNodes(nodes.toList()); } @@ -65,6 +68,29 @@ extension PasteFromPlainText on EditorState { AppFlowyRichTextKeys.href: plainText, }); await apply(transaction); + checkToShowPasteAsMenu(node); return true; } + + void checkToShowPasteAsMenu(Node node) { + if (selection == null || !selection!.isCollapsed) return; + if (UniversalPlatform.isMobile) return; + final href = _getLinkFromNode(node); + if (href != null) { + final context = document.root.context; + if (context != null && context.mounted) { + PasteAsMenuService(context: context, editorState: this).show(href); + } + } + } + + String? _getLinkFromNode(Node node) { + final delta = node.delta; + if (delta == null) return null; + final inserts = delta.whereType(); + if (inserts.isEmpty || inserts.length > 1) return null; + final link = inserts.first.attributes?.href; + if (link != null) return inserts.first.text; + return null; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index bc14073193..a6e1a78299 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -304,11 +304,14 @@ void showLinkCreateMenu( return (left, top, right, bottom, alignment); } -ShapeDecoration buildToolbarLinkDecoration(BuildContext context) => +ShapeDecoration buildToolbarLinkDecoration( + BuildContext context, { + double radius = 12.0, +}) => ShapeDecoration( color: Theme.of(context).cardColor, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(radius), ), shadows: [ const BoxShadow( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index 1abf70028f..c992e40c61 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -6,6 +6,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; import 'package:appflowy/startup/startup.dart'; @@ -14,6 +17,7 @@ import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -139,6 +143,7 @@ class _LinkHoverTriggerState extends State { isHoverMenuHovering = false; tryToDismissLinkHoverMenu(); }, + onConvertTo: (type) => convertLinkTo(editorState, selection, type), onOpenLink: openLink, onCopyLink: () => copyLink(context), onEditLink: showLinkEditMenu, @@ -236,14 +241,7 @@ class _LinkHoverTriggerState extends State { Future copyLink(BuildContext context) async { final href = widget.attribute.href ?? ''; - if (href.isEmpty) return; - await getIt() - .setData(ClipboardServiceData(plainText: href)); - if (context.mounted) { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkSuccess.tr(), - ); - } + await context.copyLink(href); hoverMenuController.close(); } @@ -270,6 +268,26 @@ class _LinkHoverTriggerState extends State { editorState.apply(transaction); } + Future convertLinkTo( + EditorState editorState, + Selection selection, + LinkConvertMenuCommand type, + ) async { + final url = widget.attribute.href ?? ''; + if (type == LinkConvertMenuCommand.toBookmark) { + await convertUrlToLinkPreview(editorState, selection, url); + } else if (type == LinkConvertMenuCommand.toMention) { + await convertUrlToMention(editorState, selection); + } else if (type == LinkConvertMenuCommand.toEmbed) { + await convertUrlToLinkPreview( + editorState, + selection, + url, + previewType: LinkEmbedKeys.embed, + ); + } + } + void onRemoveAndReplaceLink( EditorState editorState, Selection selection, @@ -307,6 +325,7 @@ class LinkHoverMenu extends StatefulWidget { required this.onOpenLink, required this.onEditLink, required this.onRemoveLink, + required this.onConvertTo, }); final Attributes attribute; @@ -317,6 +336,7 @@ class LinkHoverMenu extends StatefulWidget { final VoidCallback onOpenLink; final VoidCallback onEditLink; final VoidCallback onRemoveLink; + final ValueChanged onConvertTo; @override State createState() => _LinkHoverMenuState(); @@ -326,6 +346,8 @@ class _LinkHoverMenuState extends State { ViewPB? currentView; late bool isPage = widget.attribute.isPage; late String href = widget.attribute.href ?? ''; + final popoverController = PopoverController(); + bool isConvertButtonSelected = false; @override void initState() { @@ -333,6 +355,12 @@ class _LinkHoverMenuState extends State { if (isPage) getPageView(); } + @override + void dispose() { + super.dispose(); + popoverController.close(); + } + @override Widget build(BuildContext context) { return Column( @@ -364,6 +392,7 @@ class _LinkHoverMenuState extends State { FlowyIconButton( icon: FlowySvg(FlowySvgs.toolbar_link_m), tooltipText: LocaleKeys.editor_copyLink.tr(), + preferBelow: false, width: 36, height: 32, onPressed: widget.onCopyLink, @@ -371,13 +400,16 @@ class _LinkHoverMenuState extends State { FlowyIconButton( icon: FlowySvg(FlowySvgs.toolbar_link_edit_m), tooltipText: LocaleKeys.editor_editLink.tr(), + preferBelow: false, width: 36, height: 32, onPressed: widget.onEditLink, ), + buildConvertButton(), FlowyIconButton( icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), tooltipText: LocaleKeys.editor_removeLink.tr(), + preferBelow: false, width: 36, height: 32, onPressed: widget.onRemoveLink, @@ -444,6 +476,73 @@ class _LinkHoverMenuState extends State { ), ); } + + Widget buildConvertButton() { + return AppFlowyPopover( + offset: Offset(44, 10.0), + direction: PopoverDirection.bottomWithRightAligned, + margin: EdgeInsets.zero, + controller: popoverController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + popupBuilder: (context) => buildConvertMenu(), + child: FlowyIconButton( + icon: FlowySvg(FlowySvgs.turninto_m), + isSelected: isConvertButtonSelected, + tooltipText: LocaleKeys.editor_convertTo.tr(), + preferBelow: false, + width: 36, + height: 32, + onPressed: () { + setState(() { + isConvertButtonSelected = true; + }); + showConvertMenu(); + }, + ), + ); + } + + Widget buildConvertMenu() { + return MouseRegion( + onEnter: widget.onEnter, + onExit: widget.onExit, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: + List.generate(LinkConvertMenuCommand.values.length, (index) { + final command = LinkConvertMenuCommand.values[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () { + widget.onConvertTo(command); + closeConvertMenu(); + }, + ), + ); + }), + ), + ), + ); + } + + void showConvertMenu() { + keepEditorFocusNotifier.increase(); + popoverController.show(); + } + + void closeConvertMenu() { + popoverController.close(); + } } class HoverTriggerKey { @@ -489,3 +588,48 @@ class LinkHoverTriggers { callbacks.first.call(); } } + +enum LinkConvertMenuCommand { + toMention, + toBookmark, + toEmbed; + + String get title { + switch (this) { + case toMention: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion + .tr(); + case toBookmark: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_toBookmark + .tr(); + case toEmbed: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed + .tr(); + } + } + + String get type { + switch (this) { + case toMention: + return MentionBlockKeys.type; + case toBookmark: + return LinkPreviewBlockKeys.type; + case toEmbed: + return LinkPreviewBlockKeys.type; + } + } +} + +extension LinkExtension on BuildContext { + Future copyLink(String link) async { + if (link.isEmpty) return; + await getIt() + .setData(ClipboardServiceData(plainText: link)); + if (mounted) { + showToastNotification( + message: LocaleKeys.shareAction_copyLinkSuccess.tr(), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart new file mode 100644 index 0000000000..d08442d779 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart @@ -0,0 +1,184 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.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'; +// ignore: implementation_imports +import 'package:appflowy_editor/src/editor/util/link_util.dart'; +import 'package:flutter/services.dart'; + +import 'link_create_menu.dart'; +import 'link_styles.dart'; + +void showReplaceMenu({ + required BuildContext context, + required EditorState editorState, + required Node node, + String? url, + required LTRB ltrb, + required ValueChanged onReplace, +}) { + OverlayEntry? overlay; + + void dismissOverlay() { + keepEditorFocusNotifier.decrease(); + overlay?.remove(); + overlay = null; + } + + keepEditorFocusNotifier.increase(); + overlay = FullScreenOverlayEntry( + top: ltrb.top, + bottom: ltrb.bottom, + left: ltrb.left, + right: ltrb.right, + dismissCallback: () => keepEditorFocusNotifier.decrease(), + builder: (context) { + return LinkReplaceMenu( + link: url ?? '', + onSubmitted: (link) async { + onReplace.call(link); + dismissOverlay(); + }, + onDismiss: dismissOverlay, + ); + }, + ).build(); + + Overlay.of(context, rootOverlay: true).insert(overlay!); +} + +class LinkReplaceMenu extends StatefulWidget { + const LinkReplaceMenu({ + super.key, + required this.onSubmitted, + required this.link, + required this.onDismiss, + }); + + final ValueChanged onSubmitted; + final VoidCallback onDismiss; + final String link; + + @override + State createState() => _LinkReplaceMenuState(); +} + +class _LinkReplaceMenuState extends State { + bool showErrorText = false; + late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); + late TextEditingController textEditingController = + TextEditingController(text: widget.link); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + } + + @override + void dispose() { + focusNode.dispose(); + textEditingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 330, + padding: EdgeInsets.all(8), + decoration: buildToolbarLinkDecoration(context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: buildLinkField()), + HSpace(8), + buildReplaceButton(), + ], + ), + ); + } + + Widget buildLinkField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 32, + child: TextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + autofocus: true, + focusNode: focusNode, + textAlign: TextAlign.left, + controller: textEditingController, + style: TextStyle( + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w400, + ), + decoration: LinkStyle.buildLinkTextFieldInputDecoration( + LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_pasteHint + .tr(), + context, + showErrorBorder: showErrorText, + ), + ), + ), + if (showErrorText) + Padding( + padding: const EdgeInsets.only(top: 4), + child: FlowyText.regular( + LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), + color: LinkStyle.textStatusError, + fontSize: 12, + figmaLineHeight: 16, + ), + ), + ], + ); + } + + Widget buildReplaceButton() { + return FlowyTextButton( + LocaleKeys.button_replace.tr(), + padding: EdgeInsets.zero, + mainAxisAlignment: MainAxisAlignment.center, + constraints: BoxConstraints(maxWidth: 78, minHeight: 32), + fontSize: 14, + lineHeight: 20 / 14, + hoverColor: LinkStyle.fillThemeThick.withAlpha(200), + fontColor: Colors.white, + fillColor: LinkStyle.fillThemeThick, + fontWeight: FontWeight.w400, + onPressed: onSubmit, + ); + } + + void onSubmit() { + final link = textEditingController.text.trim(); + if (link.isEmpty || !isUri(link)) { + setState(() { + showErrorText = true; + }); + return; + } + widget.onSubmitted.call(link); + } + + KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { + if (key is! KeyDownEvent) return KeyEventResult.ignored; + if (key.logicalKey == LogicalKeyboardKey.escape) { + widget.onDismiss.call(); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.enter) { + onSubmit(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart new file mode 100644 index 0000000000..6dc6501c03 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart @@ -0,0 +1,286 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'link_embed_menu.dart'; + +class LinkEmbedKeys { + const LinkEmbedKeys._(); + static const String previewType = 'preview_type'; + static const String embed = 'embed'; + static const String align = 'align'; +} + +Node linkEmbedNode({required String url}) => Node( + type: LinkPreviewBlockKeys.type, + attributes: { + LinkPreviewBlockKeys.url: url, + LinkEmbedKeys.previewType: LinkEmbedKeys.embed, + }, + ); + +class LinkEmbedBlockComponent extends BlockComponentStatefulWidget { + const LinkEmbedBlockComponent({ + super.key, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + required super.node, + }); + + @override + State createState() => + LinkEmbedBlockComponentState(); +} + +class LinkEmbedBlockComponentState extends State + with BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; + + EmbedLoadingStatus status = EmbedLoadingStatus.loading; + final parser = LinkParser(); + LinkInfo linkInfo = LinkInfo(); + + final showActionsNotifier = ValueNotifier(false); + bool isMenuShowing = false, isHovering = false; + + @override + void initState() { + super.initState(); + parser.addLinkInfoListener((v) { + if (mounted) { + setState(() { + if (v.isEmpty() && linkInfo.isEmpty()) { + status = EmbedLoadingStatus.error; + } else { + linkInfo = v; + status = EmbedLoadingStatus.idle; + } + }); + } + }); + parser.start(url); + } + + @override + void dispose() { + parser.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget result = MouseRegion( + onEnter: (_) { + isHovering = true; + showActionsNotifier.value = true; + }, + onExit: (_) { + isHovering = false; + Future.delayed(const Duration(milliseconds: 200), () { + if (isMenuShowing || isHovering) return; + if (mounted) showActionsNotifier.value = false; + }); + }, + child: buildChild(context), + ); + result = Padding(padding: padding, child: result); + + if (widget.showActions && widget.actionBuilder != null) { + result = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: result, + ); + } + return result; + } + + Widget buildChild(BuildContext context) { + final theme = AppFlowyTheme.of(context), + fillSceme = theme.fillColorScheme, + borderScheme = theme.borderColorScheme; + Widget child; + final isIdle = status == EmbedLoadingStatus.idle; + if (isIdle) { + child = buildContent(context); + } else { + child = buildErrorLoadingWidget(context); + } + return Container( + height: 450, + decoration: BoxDecoration( + color: isIdle ? Theme.of(context).cardColor : fillSceme.tertiaryHover, + borderRadius: BorderRadius.all(Radius.circular(16)), + border: Border.all(color: borderScheme.greyTertiary), + ), + child: Stack( + children: [ + child, + buildMenu(context), + ], + ), + ); + } + + Widget buildMenu(BuildContext context) { + return Positioned( + top: 12, + right: 12, + child: ValueListenableBuilder( + valueListenable: showActionsNotifier, + builder: (context, showActions, child) { + if (!showActions) return SizedBox.shrink(); + return LinkEmbedMenu( + editorState: context.read(), + node: node, + onReload: () { + setState(() { + status = EmbedLoadingStatus.loading; + }); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) parser.start(url); + }); + }, + onMenuShowed: () { + isMenuShowing = true; + }, + onMenuHided: () { + isMenuShowing = false; + if (!isHovering && mounted) { + showActionsNotifier.value = false; + } + }, + ); + }, + ), + ); + } + + Widget buildContent(BuildContext context) { + final theme = AppFlowyTheme.of(context), textScheme = theme.textColorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + child: FlowyNetworkImage( + url: linkInfo.imageUrl ?? '', + width: MediaQuery.of(context).size.width, + ), + ), + ), + Container( + height: 64, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + child: Row( + children: [ + SizedBox.square( + dimension: 40, + child: Center( + child: linkInfo.buildIconWidget(size: Size.square(32)), + ), + ), + HSpace(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + linkInfo.siteName ?? '', + color: textScheme.primary, + fontSize: 14, + figmaLineHeight: 20, + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + ), + VSpace(4), + FlowyText.regular( + url, + color: textScheme.secondary, + fontSize: 12, + figmaLineHeight: 16, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget buildErrorLoadingWidget(BuildContext context) { + final theme = AppFlowyTheme.of(context), textSceme = theme.textColorScheme; + final isLoading = status == EmbedLoadingStatus.loading; + return isLoading + ? Center( + child: SizedBox.square( + dimension: 64, + child: CircularProgressIndicator.adaptive(), + ), + ) + : Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + FlowySvgs.embed_error_xl.path, + ), + VSpace(4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: RichText( + maxLines: 1, + overflow: TextOverflow.ellipsis, + text: TextSpan( + children: [ + TextSpan( + text: '$url ', + style: TextStyle( + color: textSceme.primary, + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w700, + ), + ), + TextSpan( + text: LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_refuseConnect + .tr(), + style: TextStyle( + color: textSceme.primary, + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +enum EmbedLoadingStatus { loading, idle, error } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart new file mode 100644 index 0000000000..8b9ac122a8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart @@ -0,0 +1,332 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/widgets.dart'; + +import 'link_embed_block_component.dart'; + +class LinkEmbedMenu extends StatefulWidget { + const LinkEmbedMenu({ + super.key, + required this.node, + required this.editorState, + required this.onMenuShowed, + required this.onMenuHided, + required this.onReload, + }); + + final Node node; + final EditorState editorState; + final VoidCallback onMenuShowed; + final VoidCallback onMenuHided; + final VoidCallback onReload; + + @override + State createState() => _LinkEmbedMenuState(); +} + +class _LinkEmbedMenuState extends State { + final turnintoController = PopoverController(); + final moreOptionController = PopoverController(); + int turnintoMenuNum = 0, moreOptionNum = 0, alignMenuNum = 0; + final moreOptionButtonKey = GlobalKey(); + bool get isTurnIntoShowing => turnintoMenuNum > 0; + bool get isMoreOptionShowing => moreOptionNum > 0; + bool get isAlignMenuShowing => alignMenuNum > 0; + + Node get node => widget.node; + EditorState get editorState => widget.editorState; + + String get url => node.attributes[LinkPreviewBlockKeys.url] ?? ''; + + @override + void dispose() { + super.dispose(); + turnintoController.close(); + moreOptionController.close(); + widget.onMenuHided.call(); + } + + @override + Widget build(BuildContext context) { + return buildChild(); + } + + Widget buildChild() { + final theme = AppFlowyTheme.of(context), + iconScheme = theme.iconColorTheme, + fillScheme = theme.fillColorScheme; + + return Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: fillScheme.primaryAlpha80, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // FlowyIconButton( + // icon: FlowySvg( + // FlowySvgs.embed_fullscreen_m, + // color: iconScheme.tertiary, + // ), + // tooltipText: LocaleKeys.document_imageBlock_openFullScreen.tr(), + // preferBelow: false, + // onPressed: () {}, + // ), + FlowyIconButton( + icon: FlowySvg( + FlowySvgs.toolbar_link_m, + color: iconScheme.tertiary, + ), + tooltipText: LocaleKeys.editor_copyLink.tr(), + preferBelow: false, + onPressed: () => copyLink(context), + ), + buildTurnIntoBotton(), + buildMoreOptionBotton(), + ], + ), + ); + } + + Widget buildTurnIntoBotton() { + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + return AppFlowyPopover( + offset: Offset(0, 6), + direction: PopoverDirection.bottomWithRightAligned, + margin: EdgeInsets.zero, + controller: turnintoController, + onOpen: () { + keepEditorFocusNotifier.increase(); + turnintoMenuNum++; + }, + onClose: () { + keepEditorFocusNotifier.decrease(); + turnintoMenuNum--; + checkToHideMenu(); + }, + popupBuilder: (context) => buildTurnIntoMenu(), + child: FlowyIconButton( + icon: FlowySvg( + FlowySvgs.turninto_m, + color: iconScheme.tertiary, + ), + tooltipText: LocaleKeys.document_toolbar_turnInto.tr(), + preferBelow: false, + onPressed: showTurnIntoMenu, + ), + ); + } + + Widget buildTurnIntoMenu() { + final types = + PasteMenuType.values.where((e) => e != PasteMenuType.embed).toList(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: List.generate(types.length, (index) { + final command = types[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () { + if (command == PasteMenuType.bookmark) { + final transaction = editorState.transaction; + transaction.updateNode(node, { + LinkPreviewBlockKeys.url: url, + LinkEmbedKeys.previewType: '', + }); + editorState.apply(transaction); + } else if (command == PasteMenuType.mention) { + convertUrlPreviewNodeToMention(editorState, node); + } else if (command == PasteMenuType.url) { + convertUrlPreviewNodeToLink(editorState, node); + } + }, + ), + ); + }), + ), + ); + } + + Widget buildMoreOptionBotton() { + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + return AppFlowyPopover( + offset: Offset(0, 6), + direction: PopoverDirection.bottomWithRightAligned, + margin: EdgeInsets.zero, + controller: moreOptionController, + onOpen: () { + keepEditorFocusNotifier.increase(); + moreOptionNum++; + }, + onClose: () { + keepEditorFocusNotifier.decrease(); + moreOptionNum--; + checkToHideMenu(); + }, + popupBuilder: (context) => buildMoreOptionMenu(), + child: FlowyIconButton( + key: moreOptionButtonKey, + icon: FlowySvg( + FlowySvgs.toolbar_more_m, + color: iconScheme.tertiary, + ), + tooltipText: LocaleKeys.document_toolbar_moreOptions.tr(), + preferBelow: false, + onPressed: showMoreOptionMenu, + ), + ); + } + + Widget buildMoreOptionMenu() { + final types = LinkEmbedMenuCommand.values; + return Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: List.generate(types.length, (index) { + final command = types[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () => onEmbedMenuCommand(command), + ), + ); + }), + ), + ); + } + + void showTurnIntoMenu() { + keepEditorFocusNotifier.increase(); + turnintoController.show(); + checkToShowMenu(); + turnintoMenuNum++; + if (isMoreOptionShowing) closeMoreOptionMenu(); + } + + void closeTurnIntoMenu() { + turnintoController.close(); + checkToHideMenu(); + } + + void showMoreOptionMenu() { + keepEditorFocusNotifier.increase(); + moreOptionController.show(); + checkToShowMenu(); + moreOptionNum++; + if (isTurnIntoShowing) closeTurnIntoMenu(); + } + + void closeMoreOptionMenu() { + moreOptionController.close(); + checkToHideMenu(); + } + + void checkToHideMenu() { + Future.delayed(Duration(milliseconds: 200), () { + if (!mounted) return; + if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { + widget.onMenuHided.call(); + } + }); + } + + void checkToShowMenu() { + if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { + widget.onMenuShowed.call(); + } + } + + Future copyLink(BuildContext context) async { + await context.copyLink(url); + widget.onMenuHided.call(); + } + + void onEmbedMenuCommand(LinkEmbedMenuCommand command) { + switch (command) { + case LinkEmbedMenuCommand.openLink: + afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); + break; + case LinkEmbedMenuCommand.replace: + final box = moreOptionButtonKey.currentContext?.findRenderObject() + as RenderBox?; + if (box == null) return; + final p = box.localToGlobal(Offset.zero); + showReplaceMenu( + context: context, + editorState: editorState, + node: node, + url: url, + ltrb: LTRB(left: p.dx - 330, top: p.dy), + onReplace: (url) async { + await convertLinkBlockToOtherLinkBlock( + editorState, + node, + node.type, + url: url, + ); + }, + ); + break; + case LinkEmbedMenuCommand.reload: + widget.onReload.call(); + break; + case LinkEmbedMenuCommand.removeLink: + removeUrlPreviewLink(editorState, node); + break; + } + closeMoreOptionMenu(); + } +} + +enum LinkEmbedMenuCommand { + openLink, + replace, + reload, + removeLink; + + String get title { + switch (this) { + case openLink: + return LocaleKeys.editor_openLink.tr(); + case replace: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace + .tr(); + case reload: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload + .tr(); + case removeLink: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_removeLink + .tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart new file mode 100644 index 0000000000..0032d9cd33 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:favicon/favicon.dart'; +import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages +import 'package:flutter_link_previewer/flutter_link_previewer.dart' hide Size; + +class LinkParser { + static final LinkInfoCache _cache = LinkInfoCache(); + final Set> _listeners = >{}; + + Future start(String url) async { + final data = await _cache.get(url); + if (data != null) { + refreshLinkInfo(data); + } + await _getLinkInfo(url); + } + + Future _getLinkInfo(String url) async { + try { + final previewData = await getPreviewData(url); + final favicon = await FaviconFinder.getBest(url); + final linkInfo = LinkInfo( + siteName: previewData.title, + description: previewData.description, + imageUrl: previewData.image?.url, + faviconUrl: favicon?.url, + ); + if (!linkInfo.isEmpty()) await _cache.set(url, linkInfo); + refreshLinkInfo(linkInfo); + return linkInfo; + } catch (e, s) { + Log.error('get link info error: ', e, s); + refreshLinkInfo(LinkInfo()); + return null; + } + } + + void refreshLinkInfo(LinkInfo info) { + for (final listener in _listeners) { + listener(info); + } + } + + void addLinkInfoListener(ValueChanged listener) { + _listeners.add(listener); + } + + void dispose() { + _listeners.clear(); + } +} + +class LinkInfo { + factory LinkInfo.fromJson(Map json) => LinkInfo( + siteName: json['siteName'], + description: json['description'], + imageUrl: json['imageUrl'], + faviconUrl: json['faviconUrl'], + ); + + LinkInfo({ + this.siteName, + this.description, + this.imageUrl, + this.faviconUrl, + }); + + final String? siteName; + final String? description; + final String? imageUrl; + final String? faviconUrl; + + Map toJson() => { + 'siteName': siteName, + 'description': description, + 'imageUrl': imageUrl, + 'faviconUrl': faviconUrl, + }; + + bool isEmpty() { + return siteName == null || + description == null || + imageUrl == null || + faviconUrl == null; + } + + Widget buildIconWidget({Size size = const Size.square(20.0)}) { + final iconUrl = faviconUrl; + if (iconUrl == null) { + return FlowySvg(FlowySvgs.toolbar_link_earth_m, size: size); + } + if (iconUrl.endsWith('.svg')) { + return FlowyNetworkSvg( + iconUrl, + height: size.height, + width: size.width, + errorWidget: const FlowySvg(FlowySvgs.toolbar_link_earth_m), + ); + } + return FlowyNetworkImage( + url: iconUrl, + fit: BoxFit.contain, + height: size.height, + width: size.width, + errorWidgetBuilder: (context, error, stackTrace) => + const FlowySvg(FlowySvgs.toolbar_link_earth_m), + ); + } +} + +class LinkInfoCache { + final _linkInfoPrefix = 'link_info'; + + Future get(String url) async { + final option = await getIt().getWithFormat( + _linkInfoPrefix + url, + (value) => LinkInfo.fromJson(jsonDecode(value)), + ); + return option; + } + + Future set(String url, LinkInfo data) async { + await getIt().set( + _linkInfoPrefix + url, + jsonEncode(data.toJson()), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 879a71f008..0b6ad9fd7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; class CustomLinkPreviewWidget extends StatelessWidget { const CustomLinkPreviewWidget({ @@ -21,6 +22,8 @@ class CustomLinkPreviewWidget extends StatelessWidget { this.title, this.description, this.imageUrl, + this.isHovering = false, + this.status = LinkPreviewStatus.loading, }); final Node node; @@ -28,9 +31,14 @@ class CustomLinkPreviewWidget extends StatelessWidget { final String? description; final String? imageUrl; final String url; + final bool isHovering; + final LinkPreviewStatus status; @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context), + borderScheme = theme.borderColorScheme, + textScheme = theme.textColorScheme; final documentFontSize = context .read() .editorStyle @@ -39,69 +47,60 @@ class CustomLinkPreviewWidget extends StatelessWidget { .fontSize ?? 16.0; final (fontSize, width) = UniversalPlatform.isDesktopOrWeb - ? (documentFontSize, 180.0) + ? (documentFontSize, 160.0) : (documentFontSize - 2, 120.0); final Widget child = Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.onSurface, - ), - borderRadius: BorderRadius.circular( - 6.0, + color: isHovering + ? borderScheme.greyTertiaryHover + : borderScheme.greyTertiary, ), + borderRadius: BorderRadius.circular(16.0), ), - child: IntrinsicHeight( + child: SizedBox( + height: 96, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (imageUrl != null) - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(6.0), - bottomLeft: Radius.circular(6.0), - ), - child: FlowyNetworkImage( - url: imageUrl!, - width: width, - ), - ), + buildImage(context), Expanded( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.fromLTRB(20, 12, 60, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + buildLoadingOrErrorWidget(), if (title != null) Padding( - padding: const EdgeInsets.only( - bottom: 4.0, - right: 10.0, - ), + padding: const EdgeInsets.only(bottom: 4.0), child: FlowyText.medium( title!, - maxLines: 2, overflow: TextOverflow.ellipsis, fontSize: fontSize, + color: textScheme.primary, + figmaLineHeight: 20, ), ), if (description != null) Padding( - padding: const EdgeInsets.only(bottom: 4.0), + padding: const EdgeInsets.only(bottom: 16.0), child: FlowyText( description!, - maxLines: 2, overflow: TextOverflow.ellipsis, fontSize: fontSize - 4, + figmaLineHeight: 16, + color: textScheme.primary, ), ), FlowyText( url.toString(), overflow: TextOverflow.ellipsis, - maxLines: 2, - color: Theme.of(context).hintColor, + color: textScheme.secondary, fontSize: fontSize - 4, + figmaLineHeight: 16, ), ], ), @@ -150,4 +149,67 @@ class CustomLinkPreviewWidget extends StatelessWidget { ), ]; } + + Widget buildImage(BuildContext context) { + final theme = AppFlowyTheme.of(context), + fillScheme = theme.fillColorScheme, + iconScheme = theme.iconColorTheme; + final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; + Widget child; + if (imageUrl != null) { + child = FlowyNetworkImage( + url: imageUrl!, + width: width, + ); + } else { + child = Center( + child: FlowySvg( + FlowySvgs.toolbar_link_earth_m, + color: iconScheme.secondary, + size: Size.square(30), + ), + ); + } + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16.0), + bottomLeft: Radius.circular(16.0), + ), + child: Container( + width: width, + color: fillScheme.quaternary, + child: child, + ), + ); + } + + Widget buildLoadingOrErrorWidget() { + if (status == LinkPreviewStatus.loading) { + return Expanded( + child: const Center( + child: SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), + ), + ), + ); + } else if (status == LinkPreviewStatus.error) { + return Expanded( + child: const Center( + child: SizedBox( + height: 16, + width: 16, + child: Icon( + Icons.error_outline, + color: Colors.red, + ), + ), + ), + ); + } + return SizedBox.shrink(); + } } + +enum LinkPreviewStatus { loading, error, idle } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart new file mode 100644 index 0000000000..3772169331 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart @@ -0,0 +1,204 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:flutter/material.dart'; + +import 'custom_link_preview.dart'; +import 'link_preview_menu.dart'; + +class CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder { + CustomLinkPreviewBlockComponentBuilder({ + super.configuration, + this.cache, + }); + + final LinkPreviewDataCacheInterface? cache; + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + final isEmbed = + node.attributes[LinkEmbedKeys.previewType] == LinkEmbedKeys.embed; + if (isEmbed) { + return LinkEmbedBlockComponent( + key: node.key, + node: node, + configuration: configuration, + showActions: showActions(node), + actionBuilder: (_, state) => + actionBuilder(blockComponentContext, state), + ); + } + return CustomLinkPreviewBlockComponent( + key: node.key, + node: node, + configuration: configuration, + showActions: showActions(node), + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), + cache: cache, + ); + } + + @override + BlockComponentValidate get validate => + (node) => node.attributes[LinkPreviewBlockKeys.url]!.isNotEmpty; +} + +class CustomLinkPreviewBlockComponent extends BlockComponentStatefulWidget { + const CustomLinkPreviewBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + this.cache, + }); + + final LinkPreviewDataCacheInterface? cache; + + @override + State createState() => + CustomLinkPreviewBlockComponentState(); +} + +class CustomLinkPreviewBlockComponentState + extends State + with BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; + + late final LinkPreviewParser parser; + late Future future; + + final showActionsNotifier = ValueNotifier(false); + bool isMenuShowing = false, isHovering = false; + + @override + void initState() { + super.initState(); + parser = LinkPreviewParser(url: url, cache: widget.cache); + future = parser.start(); + } + + @override + void didUpdateWidget(CustomLinkPreviewBlockComponent oldWidget) { + super.didUpdateWidget(oldWidget); + final url = widget.node.attributes[LinkPreviewBlockKeys.url]!; + final oldUrl = oldWidget.node.attributes[LinkPreviewBlockKeys.url]!; + if (url != oldUrl) { + parser = LinkPreviewParser(url: url, cache: widget.cache); + setState(() { + future = parser.start(); + }); + } + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) { + isHovering = true; + showActionsNotifier.value = true; + }, + onExit: (_) { + isHovering = false; + Future.delayed(const Duration(milliseconds: 200), () { + if (isMenuShowing || isHovering) return; + if (mounted) showActionsNotifier.value = false; + }); + }, + hitTestBehavior: HitTestBehavior.opaque, + opaque: false, + child: ValueListenableBuilder( + valueListenable: showActionsNotifier, + builder: (context, showActions, child) { + return FutureBuilder( + future: future, + builder: (context, snapshot) { + Widget child; + + if (snapshot.connectionState != ConnectionState.done) { + child = CustomLinkPreviewWidget( + node: node, + url: url, + isHovering: showActions, + ); + } else { + final title = parser.getContent(LinkPreviewRegex.title); + final description = + parser.getContent(LinkPreviewRegex.description); + final image = parser.getContent(LinkPreviewRegex.image); + + if (title == null && description == null && image == null) { + child = CustomLinkPreviewWidget( + node: node, + url: url, + isHovering: showActions, + status: LinkPreviewStatus.error, + ); + } else { + child = CustomLinkPreviewWidget( + node: node, + url: url, + title: title, + description: description, + imageUrl: image, + isHovering: showActions, + status: LinkPreviewStatus.idle, + ); + } + } + + child = Padding(padding: padding, child: child); + + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + child = Stack( + children: [ + child, + if (showActions) + Positioned( + top: 16, + right: 16, + child: CustomLinkPreviewMenu( + onMenuShowed: () { + isMenuShowing = true; + }, + onMenuHided: () { + isMenuShowing = false; + if (!isHovering && mounted) { + showActionsNotifier.value = false; + } + }, + onReload: () { + if (mounted) { + setState(() { + future = parser.start(); + }); + } + }, + node: node, + ), + ), + ], + ); + + return child; + }, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index 2ba67a87da..2fb493dda3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -1,109 +1,207 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -import '../image/custom_image_block_component/custom_image_block_component.dart'; - -class LinkPreviewMenu extends StatefulWidget { - const LinkPreviewMenu({ +class CustomLinkPreviewMenu extends StatefulWidget { + const CustomLinkPreviewMenu({ super.key, + required this.onMenuShowed, + required this.onMenuHided, + required this.onReload, required this.node, - required this.state, }); - + final VoidCallback onMenuShowed; + final VoidCallback onMenuHided; + final VoidCallback onReload; final Node node; - final LinkPreviewBlockComponentState state; @override - State createState() => _LinkPreviewMenuState(); + State createState() => _CustomLinkPreviewMenuState(); } -class _LinkPreviewMenuState extends State { +class _CustomLinkPreviewMenuState extends State { + final popoverController = PopoverController(); + final buttonKey = GlobalKey(); + bool closed = false; + bool selected = false; + + @override + void dispose() { + super.dispose(); + popoverController.close(); + widget.onMenuHided.call(); + } + @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Container( - height: 32, - decoration: BoxDecoration( - color: theme.cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(4.0), - ), - child: Row( - children: [ - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(), - iconData: FlowySvgs.m_toolbar_link_m, - onTap: () async => convertUrlPreviewNodeToLink( - context.read(), - widget.node, - ), - ), - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.editor_copyLink.tr(), - iconData: FlowySvgs.copy_s, - onTap: copyImageLink, - ), - const _Divider(), - MenuBlockButton( - tooltip: LocaleKeys.button_delete.tr(), - iconData: FlowySvgs.trash_s, - onTap: deleteLinkPreviewNode, - ), - const HSpace(4), - ], + return AppFlowyPopover( + offset: Offset(0, 0.0), + direction: PopoverDirection.bottomWithRightAligned, + margin: EdgeInsets.zero, + controller: popoverController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () { + keepEditorFocusNotifier.decrease(); + if (!closed) { + closed = true; + return; + } else { + closed = false; + widget.onMenuHided.call(); + } + setState(() { + selected = false; + }); + }, + popupBuilder: (context) => buildMenu(), + child: FlowyIconButton( + key: buttonKey, + isSelected: selected, + icon: FlowySvg(FlowySvgs.toolbar_more_m), + onPressed: showPopover, ), ); } - void copyImageLink() { - final url = widget.node.attributes[CustomImageBlockKeys.url]; - if (url != null) { - Clipboard.setData(ClipboardData(text: url)); - showToastNotification( - message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(), - ); + Widget buildMenu() { + return MouseRegion( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: + List.generate(LinkPreviewMenuCommand.values.length, (index) { + final command = LinkPreviewMenuCommand.values[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () => onTap(command), + ), + ); + }), + ), + ), + ); + } + + Future onTap(LinkPreviewMenuCommand command) async { + final editorState = context.read(); + final node = widget.node; + final url = node.attributes[LinkPreviewBlockKeys.url]; + switch (command) { + case LinkPreviewMenuCommand.convertToMention: + await convertUrlPreviewNodeToMention(editorState, node); + break; + case LinkPreviewMenuCommand.convertToUrl: + await convertUrlPreviewNodeToLink(editorState, node); + break; + case LinkPreviewMenuCommand.convertToEmbed: + final transaction = editorState.transaction; + transaction.updateNode(node, { + LinkPreviewBlockKeys.url: url, + LinkEmbedKeys.previewType: LinkEmbedKeys.embed, + }); + await editorState.apply(transaction); + break; + case LinkPreviewMenuCommand.copyLink: + if (url != null) { + await context.copyLink(url); + } + break; + case LinkPreviewMenuCommand.replace: + final box = buttonKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return; + final p = box.localToGlobal(Offset.zero); + showReplaceMenu( + context: context, + editorState: editorState, + node: node, + url: url, + ltrb: LTRB(left: p.dx - 330, top: p.dy), + onReplace: (url) async { + await convertLinkBlockToOtherLinkBlock( + editorState, + node, + node.type, + url: url, + ); + }, + ); + break; + case LinkPreviewMenuCommand.reload: + widget.onReload.call(); + break; + case LinkPreviewMenuCommand.removeLink: + await removeUrlPreviewLink(editorState, node); + break; + } + closePopover(); + } + + void showPopover() { + widget.onMenuShowed.call(); + keepEditorFocusNotifier.increase(); + popoverController.show(); + setState(() { + selected = true; + }); + } + + void closePopover() { + popoverController.close(); + widget.onMenuHided.call(); + } +} + +enum LinkPreviewMenuCommand { + convertToMention, + convertToUrl, + convertToEmbed, + copyLink, + replace, + reload, + removeLink; + + String get title { + switch (this) { + case convertToMention: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion + .tr(); + case LinkPreviewMenuCommand.convertToUrl: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl + .tr(); + case LinkPreviewMenuCommand.convertToEmbed: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed + .tr(); + case LinkPreviewMenuCommand.copyLink: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink + .tr(); + case LinkPreviewMenuCommand.replace: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace + .tr(); + case LinkPreviewMenuCommand.reload: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload + .tr(); + case LinkPreviewMenuCommand.removeLink: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_removeLink + .tr(); } } - - Future deleteLinkPreviewNode() async { - final node = widget.node; - final editorState = context.read(); - final transaction = editorState.transaction; - transaction.deleteNode(node); - transaction.afterSelection = null; - await editorState.apply(transaction); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Container( - width: 1, - color: Colors.grey, - ), - ); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart new file mode 100644 index 0000000000..16febe54cb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart @@ -0,0 +1,253 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension_v2.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +const _menuHeighgt = 188.0, _menuWidth = 288.0; + +class PasteAsMenuService { + PasteAsMenuService({ + required this.context, + required this.editorState, + }); + + final BuildContext context; + final EditorState editorState; + OverlayEntry? _menuEntry; + + void show(String href) { + WidgetsBinding.instance.addPostFrameCallback((_) => _show(href)); + } + + void dismiss() { + if (_menuEntry != null) { + editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); + keepEditorFocusNotifier.decrease(); + } + + _menuEntry?.remove(); + _menuEntry = null; + } + + void _show(String href) { + final Size editorSize = editorState.renderBox?.size ?? Size.zero; + if (editorSize == Size.zero) return; + final menuPosition = editorState.calculateMenuOffset( + menuWidth: _menuWidth, + menuHeight: _menuHeighgt, + ); + if (menuPosition == null) return; + final ltrb = menuPosition.ltrb; + + _menuEntry = OverlayEntry( + builder: (context) => SizedBox( + height: editorSize.height, + width: editorSize.width, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: dismiss, + child: Stack( + children: [ + ltrb.buildPositioned( + child: PasteAsMenu( + onSelect: (t) { + final selection = editorState.selection; + if (selection == null) return; + final end = selection.end; + final urlSelection = Selection( + start: end.copyWith(offset: end.offset - href.length), + end: end, + ); + if (t == PasteMenuType.bookmark) { + convertUrlToLinkPreview(editorState, urlSelection, href); + } else if (t == PasteMenuType.mention) { + convertUrlToMention(editorState, urlSelection); + } else if (t == PasteMenuType.embed) { + convertUrlToLinkPreview( + editorState, + urlSelection, + href, + previewType: LinkEmbedKeys.embed, + ); + } + dismiss(); + }, + onDismiss: dismiss, + ), + ), + ], + ), + ), + ), + ); + + Overlay.of(context).insert(_menuEntry!); + + editorState.service.keyboardService?.disable(showCursor: true); + editorState.service.scrollService?.disable(); + } +} + +class PasteAsMenu extends StatefulWidget { + const PasteAsMenu({ + super.key, + required this.onSelect, + required this.onDismiss, + }); + final ValueChanged onSelect; + final VoidCallback onDismiss; + + @override + State createState() => _PasteAsMenuState(); +} + +class _PasteAsMenuState extends State { + final focusNode = FocusNode(debugLabel: 'paste_as_menu'); + final ValueNotifier selectedIndexNotifier = ValueNotifier(0); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => focusNode.requestFocus(), + ); + } + + @override + void dispose() { + focusNode.dispose(); + selectedIndexNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final themeV2 = AFThemeExtensionV2.of(context); + return Focus( + focusNode: focusNode, + onKeyEvent: onKeyEvent, + child: Container( + width: _menuWidth, + height: _menuHeighgt, + padding: EdgeInsets.all(6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 16, + color: themeV2.shadow_medium, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 32, + padding: EdgeInsets.all(8), + child: FlowyText.semibold( + color: themeV2.text_tertiary, + LocaleKeys.document_plugins_linkPreview_typeSelection_pasteAs + .tr(), + ), + ), + ...List.generate( + PasteMenuType.values.length, + (i) => buildItem(PasteMenuType.values[i], i), + ), + ], + ), + ), + ); + } + + Widget buildItem(PasteMenuType type, int i) { + return ValueListenableBuilder( + valueListenable: selectedIndexNotifier, + builder: (context, value, child) { + final isSelected = i == value; + return SizedBox( + height: 36, + child: FlowyButton( + isSelected: isSelected, + text: FlowyText( + type.title, + ), + onTap: () => onSelect(type), + ), + ); + }, + ); + } + + void changeIndex(int index) => selectedIndexNotifier.value = index; + + KeyEventResult onKeyEvent(focus, KeyEvent event) { + if (event is! KeyDownEvent && event is! KeyRepeatEvent) { + return KeyEventResult.ignored; + } + + int index = selectedIndexNotifier.value, + length = PasteMenuType.values.length; + if (event.logicalKey == LogicalKeyboardKey.enter) { + onSelect(PasteMenuType.values[index]); + } else if (event.logicalKey == LogicalKeyboardKey.escape) { + dismiss(); + } else if ([LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowLeft] + .contains(event.logicalKey)) { + if (index == 0) { + index = length - 1; + } else { + index--; + } + changeIndex(index); + } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowRight] + .contains(event.logicalKey)) { + if (index == length - 1) { + index = 0; + } else { + index++; + } + changeIndex(index); + } + return KeyEventResult.handled; + } + + void onSelect(PasteMenuType type) => widget.onSelect.call(type); + + void dismiss() => widget.onDismiss.call(); +} + +enum PasteMenuType { + mention, + url, + bookmark, + embed, +} + +extension PasteMenuTypeExtension on PasteMenuType { + String get title { + switch (this) { + case PasteMenuType.mention: + return LocaleKeys.document_plugins_linkPreview_typeSelection_mention + .tr(); + case PasteMenuType.url: + return LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr(); + case PasteMenuType.bookmark: + return LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark + .tr(); + case PasteMenuType.embed: + return LocaleKeys.document_plugins_linkPreview_typeSelection_embed.tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart index 57564c4722..8b193c70fb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; @@ -9,7 +11,7 @@ Future convertUrlPreviewNodeToLink( return; } - final url = node.attributes[ImageBlockKeys.url]; + final url = node.attributes[LinkPreviewBlockKeys.url]; final delta = Delta() ..insert( url, @@ -29,3 +31,172 @@ Future convertUrlPreviewNodeToLink( ); return editorState.apply(transaction); } + +Future convertUrlPreviewNodeToMention( + EditorState editorState, + Node node, +) async { + if (node.type != LinkPreviewBlockKeys.type) { + return; + } + + final url = node.attributes[LinkPreviewBlockKeys.url]; + final delta = Delta() + ..insert( + MentionBlockKeys.mentionChar, + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.externalLink.name, + MentionBlockKeys.url: url, + }, + }, + ); + final transaction = editorState.transaction; + transaction + ..insertNode(node.path, paragraphNode(delta: delta)) + ..deleteNode(node); + transaction.afterSelection = Selection.collapsed( + Position( + path: node.path, + offset: url.length, + ), + ); + return editorState.apply(transaction); +} + +Future removeUrlPreviewLink( + EditorState editorState, + Node node, +) async { + if (node.type != LinkPreviewBlockKeys.type) { + return; + } + + final url = node.attributes[LinkPreviewBlockKeys.url]; + final delta = Delta()..insert(url); + final transaction = editorState.transaction; + transaction + ..insertNode(node.path, paragraphNode(delta: delta)) + ..deleteNode(node); + transaction.afterSelection = Selection.collapsed( + Position( + path: node.path, + offset: url.length, + ), + ); + return editorState.apply(transaction); +} + +Future convertUrlToLinkPreview( + EditorState editorState, + Selection selection, + String url, { + String? previewType, +}) async { + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final delta = node.delta; + if (delta == null) return; + final List beforeOperations = [], afterOperations = []; + int index = 0; + for (final insert in delta.whereType()) { + if (index < selection.startIndex) { + beforeOperations.add(insert); + } else if (index >= selection.endIndex) { + afterOperations.add(insert); + } + index += insert.length; + } + final transaction = editorState.transaction; + transaction + ..deleteNode(node) + ..insertNodes(node.path.next, [ + if (beforeOperations.isNotEmpty) + paragraphNode(delta: Delta(operations: beforeOperations)), + if (previewType == LinkEmbedKeys.embed) + linkEmbedNode(url: url) + else + linkPreviewNode(url: url), + if (afterOperations.isNotEmpty) + paragraphNode(delta: Delta(operations: afterOperations)), + ]); + await editorState.apply(transaction); +} + +Future convertUrlToMention( + EditorState editorState, + Selection selection, +) async { + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final delta = node.delta; + if (delta == null) return; + String url = ''; + int index = 0; + for (final insert in delta.whereType()) { + if (index >= selection.startIndex && index < selection.endIndex) { + final href = insert.attributes?.href ?? ''; + if (href.isNotEmpty) { + url = href; + break; + } + } + index += insert.length; + } + final transaction = editorState.transaction; + transaction.replaceText( + node, + selection.startIndex, + selection.length, + MentionBlockKeys.mentionChar, + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.externalLink.name, + MentionBlockKeys.url: url, + }, + }, + ); + await editorState.apply(transaction); +} + +Future convertLinkBlockToOtherLinkBlock( + EditorState editorState, + Node node, + String toType, { + String? url, +}) async { + final nodeType = node.type; + if (nodeType != LinkPreviewBlockKeys.type || + (nodeType == toType && url == null)) { + return; + } + final insertedNode = []; + + final afterUrl = url ?? node.attributes[LinkPreviewBlockKeys.url] ?? ''; + final previewType = node.attributes[LinkEmbedKeys.previewType]; + Node afterNode = node.copyWith( + type: toType, + attributes: { + LinkPreviewBlockKeys.url: afterUrl, + LinkEmbedKeys.previewType: previewType, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: node.attributes[blockComponentTextDirection], + blockComponentDelta: (node.delta ?? Delta()).toJson(), + }, + ); + afterNode = afterNode.copyWith(children: []); + insertedNode.add(afterNode); + insertedNode.addAll(node.children.map((e) => e.deepCopy())); + final transaction = editorState.transaction; + transaction.insertNodes( + node.path, + insertedNode, + ); + transaction.deleteNodes([node]); + await editorState.apply(transaction); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 79c9b31d20..0060d65bb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -6,14 +6,18 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'mention_link_block.dart'; + enum MentionType { page, date, + externalLink, childPage; static MentionType fromString(String value) => switch (value) { 'page' => page, 'date' => date, + 'externalLink' => externalLink, 'childPage' => childPage, // Backwards compatibility 'reminder' => date, @@ -47,6 +51,7 @@ class MentionBlockKeys { static const pageId = 'page_id'; static const blockId = 'block_id'; + static const url = 'url'; // Related to Reminder and Date blocks static const date = 'date'; // Start Date @@ -157,6 +162,17 @@ class MentionBlock extends StatelessWidget { reminderOption: reminderOption ?? ReminderOption.none, includeTime: mention[MentionBlockKeys.includeTime] ?? false, ); + case MentionType.externalLink: + final String? url = mention[MentionBlockKeys.url] as String?; + if (url == null) { + return const SizedBox.shrink(); + } + return MentionLinkBlock( + url: url, + editorState: editorState, + node: node, + index: index, + ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart new file mode 100644 index 0000000000..7a807fef64 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart @@ -0,0 +1,337 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.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/services.dart'; + +import 'mention_link_error_preview.dart'; +import 'mention_link_preview.dart'; + +class MentionLinkBlock extends StatefulWidget { + const MentionLinkBlock({ + super.key, + required this.url, + required this.editorState, + required this.node, + required this.index, + this.delayToShow = const Duration(milliseconds: 50), + this.delayToHide = const Duration(milliseconds: 300), + }); + + final String url; + final Duration delayToShow; + final Duration delayToHide; + final EditorState editorState; + final Node node; + final int index; + + @override + State createState() => _MentionLinkBlockState(); +} + +class _MentionLinkBlockState extends State { + final parser = LinkParser(); + _LoadingStatus status = _LoadingStatus.loading; + final previewController = PopoverController(); + LinkInfo linkInfo = LinkInfo(); + bool isHovering = false; + int previewFocusNum = 0; + bool isPreviewHovering = false; + bool showAtBottom = false; + final key = GlobalKey(); + + bool get isPreviewShowing => previewFocusNum > 0; + String get url => widget.url; + + EditorState get editorState => widget.editorState; + + Node get node => widget.node; + + int get index => widget.index; + + bool get readyForPreview => + status == _LoadingStatus.idle && !linkInfo.isEmpty(); + + @override + void initState() { + super.initState(); + + parser.addLinkInfoListener((v) { + if (mounted) { + setState(() { + if (v.isEmpty() && linkInfo.isEmpty()) { + status = _LoadingStatus.error; + } else { + linkInfo = v; + status = _LoadingStatus.idle; + } + }); + } + }); + parser.start(url); + } + + @override + void dispose() { + super.dispose(); + parser.dispose(); + previewController.close(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + key: ValueKey(showAtBottom), + controller: previewController, + direction: showAtBottom + ? PopoverDirection.bottomWithLeftAligned + : PopoverDirection.topWithLeftAligned, + offset: Offset(0, showAtBottom ? -20 : 20), + onOpen: () { + keepEditorFocusNotifier.increase(); + previewFocusNum++; + }, + onClose: () { + keepEditorFocusNotifier.decrease(); + previewFocusNum--; + }, + decorationColor: Colors.transparent, + popoverDecoration: BoxDecoration(), + margin: EdgeInsets.zero, + constraints: getConstraints(), + borderRadius: BorderRadius.circular(16), + popupBuilder: (context) => readyForPreview + ? MentionLinkPreview( + linkInfo: linkInfo, + showAtBottom: showAtBottom, + triggerSize: getSizeFromKey(), + onEnter: (e) { + isPreviewHovering = true; + }, + onExit: (e) { + isPreviewHovering = false; + tryToDismissPreview(); + }, + onCopyLink: () => copyLink(context), + onConvertTo: (s) => convertTo(s), + onRemoveLink: removeLink, + onOpenLink: openLink, + ) + : MentionLinkErrorPreview( + url: url, + triggerSize: getSizeFromKey(), + onEnter: (e) { + isPreviewHovering = true; + }, + onExit: (e) { + isPreviewHovering = false; + tryToDismissPreview(); + }, + onCopyLink: () => copyLink(context), + onConvertTo: (s) => convertTo(s), + onRemoveLink: removeLink, + onOpenLink: openLink, + ), + child: buildIconWithTitle(context), + ); + } + + Widget buildIconWithTitle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: onEnter, + onExit: onExit, + child: GestureDetector( + onTap: () async { + await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); + }, + child: FlowyHoverContainer( + style: + HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary), + applyStyle: isHovering, + child: Row( + mainAxisSize: MainAxisSize.min, + key: key, + children: [ + HSpace(2), + buildIcon(), + HSpace(4), + Flexible( + child: FlowyText( + linkInfo.siteName ?? url, + color: theme.textColorScheme.primary, + fontSize: 14, + figmaLineHeight: 20, + overflow: TextOverflow.ellipsis, + ), + ), + HSpace(2), + ], + ), + ), + ), + ); + } + + Widget buildIcon() { + const defaultWidget = FlowySvg(FlowySvgs.toolbar_link_earth_m); + Widget icon = defaultWidget; + if (status == _LoadingStatus.loading) { + icon = Padding( + padding: const EdgeInsets.all(2.0), + child: const CircularProgressIndicator(strokeWidth: 1), + ); + } else { + icon = linkInfo.buildIconWidget(); + } + return SizedBox( + height: 20, + width: 20, + child: icon, + ); + } + + RenderBox? get box => key.currentContext?.findRenderObject() as RenderBox?; + + Size getSizeFromKey() => box?.size ?? Size.zero; + + Future copyLink(BuildContext context) async { + await context.copyLink(url); + previewController.close(); + } + + Future openLink() async { + await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); + } + + Future removeLink() async { + final transaction = editorState.transaction + ..replaceText(widget.node, widget.index, 1, url, attributes: {}); + await editorState.apply(transaction); + } + + Future convertTo(PasteMenuType type) async { + if (type == PasteMenuType.url) { + await toUrl(); + } else if (type == PasteMenuType.bookmark) { + await toLinkPreview(); + } else if (type == PasteMenuType.embed) { + await toLinkPreview(previewType: LinkEmbedKeys.embed); + } + } + + Future toUrl() async { + final transaction = editorState.transaction + ..replaceText( + widget.node, + widget.index, + 1, + url, + attributes: { + AppFlowyRichTextKeys.href: url, + }, + ); + await editorState.apply(transaction); + } + + Future toLinkPreview({String? previewType}) async { + final selection = Selection( + start: Position(path: node.path, offset: index), + end: Position(path: node.path, offset: index + 1), + ); + await convertUrlToLinkPreview( + editorState, + selection, + url, + previewType: previewType, + ); + } + + void changeHovering(bool hovering) { + if (isHovering == hovering) return; + if (mounted) { + setState(() { + isHovering = hovering; + }); + } + } + + void changeShowAtBottom(bool bottom) { + if (showAtBottom == bottom) return; + if (mounted) { + setState(() { + showAtBottom = bottom; + }); + } + } + + void tryToDismissPreview() { + Future.delayed(widget.delayToHide, () { + if (isHovering || isPreviewHovering) { + return; + } + previewController.close(); + }); + } + + void onEnter(PointerEnterEvent e) { + changeHovering(true); + final location = box?.localToGlobal(Offset.zero) ?? Offset.zero; + if (readyForPreview) { + if (location.dy < 300) { + changeShowAtBottom(true); + } else { + changeShowAtBottom(false); + } + } + Future.delayed(widget.delayToShow, () { + if (isHovering && !isPreviewShowing && status != _LoadingStatus.loading) { + showPreview(); + } + }); + } + + void onExit(PointerExitEvent e) { + changeHovering(false); + tryToDismissPreview(); + } + + void showPreview() { + if (!mounted) return; + keepEditorFocusNotifier.increase(); + previewController.show(); + previewFocusNum++; + } + + BoxConstraints getConstraints() { + final size = getSizeFromKey(); + if (!readyForPreview) { + return BoxConstraints( + maxWidth: max(320, size.width), + maxHeight: 48 + size.height, + ); + } + return BoxConstraints( + maxWidth: max(300, size.width), + maxHeight: 300, + ); + } +} + +enum _LoadingStatus { + loading, + idle, + error, +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart new file mode 100644 index 0000000000..df396108e4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart @@ -0,0 +1,232 @@ +import 'dart:math'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; +import 'package:appflowy/util/theme_extension.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'; +import 'package:flutter/services.dart'; + +class MentionLinkErrorPreview extends StatefulWidget { + const MentionLinkErrorPreview({ + super.key, + required this.url, + required this.onEnter, + required this.onExit, + required this.onCopyLink, + required this.onRemoveLink, + required this.onConvertTo, + required this.onOpenLink, + required this.triggerSize, + }); + + final String url; + final PointerEnterEventListener onEnter; + final PointerExitEventListener onExit; + final VoidCallback onCopyLink; + final VoidCallback onRemoveLink; + final VoidCallback onOpenLink; + final ValueChanged onConvertTo; + final Size triggerSize; + + @override + State createState() => + _MentionLinkErrorPreviewState(); +} + +class _MentionLinkErrorPreviewState extends State { + final menuController = PopoverController(); + bool isConvertButtonSelected = false; + + @override + void dispose() { + super.dispose(); + menuController.close(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MouseRegion( + onEnter: widget.onEnter, + onExit: widget.onExit, + child: SizedBox( + width: max(320, widget.triggerSize.width), + height: 48, + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: 320, + height: 48, + decoration: buildToolbarLinkDecoration(context), + padding: EdgeInsets.fromLTRB(12, 8, 8, 8), + child: Row( + children: [ + Expanded(child: buildLinkWidget()), + Container( + height: 20, + width: 1, + color: Color(0xffE8ECF3) + .withAlpha(Theme.of(context).isLightMode ? 255 : 40), + margin: EdgeInsets.symmetric(horizontal: 6), + ), + FlowyIconButton( + icon: FlowySvg(FlowySvgs.toolbar_link_m), + tooltipText: LocaleKeys.editor_copyLink.tr(), + preferBelow: false, + width: 36, + height: 32, + onPressed: widget.onCopyLink, + ), + buildConvertButton(), + ], + ), + ), + ), + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: widget.onEnter, + onExit: widget.onExit, + child: GestureDetector( + onTap: widget.onOpenLink, + child: Container( + width: widget.triggerSize.width, + height: widget.triggerSize.height, + color: Colors.black.withAlpha(1), + ), + ), + ), + ], + ); + } + + Widget buildLinkWidget() { + final url = widget.url; + return FlowyTooltip( + message: url, + preferBelow: false, + child: FlowyText.regular( + url, + overflow: TextOverflow.ellipsis, + figmaLineHeight: 20, + fontSize: 14, + ), + ); + } + + Widget buildConvertButton() { + return AppFlowyPopover( + offset: Offset(8, 10), + direction: PopoverDirection.bottomWithRightAligned, + margin: EdgeInsets.zero, + controller: menuController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + popupBuilder: (context) => buildConvertMenu(), + child: FlowyIconButton( + icon: FlowySvg(FlowySvgs.turninto_m), + isSelected: isConvertButtonSelected, + tooltipText: LocaleKeys.editor_convertTo.tr(), + preferBelow: false, + width: 36, + height: 32, + onPressed: () { + setState(() { + isConvertButtonSelected = true; + }); + showPopover(); + }, + ), + ); + } + + Widget buildConvertMenu() { + return MouseRegion( + onEnter: widget.onEnter, + onExit: widget.onExit, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: List.generate(MentionLinktErrorMenuCommand.values.length, + (index) { + final command = MentionLinktErrorMenuCommand.values[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () => onTap(command), + ), + ); + }), + ), + ), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + menuController.show(); + } + + void closePopover() { + menuController.close(); + } + + void onTap(MentionLinktErrorMenuCommand command) { + switch (command) { + case MentionLinktErrorMenuCommand.toURL: + widget.onConvertTo(PasteMenuType.url); + break; + case MentionLinktErrorMenuCommand.toBookmark: + widget.onConvertTo(PasteMenuType.bookmark); + break; + case MentionLinktErrorMenuCommand.toEmbed: + widget.onConvertTo(PasteMenuType.embed); + break; + case MentionLinktErrorMenuCommand.removeLink: + widget.onRemoveLink(); + break; + } + closePopover(); + } +} + +enum MentionLinktErrorMenuCommand { + toURL, + toBookmark, + toEmbed, + removeLink; + + String get title { + switch (this) { + case toURL: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl + .tr(); + case toBookmark: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_toBookmark + .tr(); + case toEmbed: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed + .tr(); + case removeLink: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_removeLink + .tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart new file mode 100644 index 0000000000..00082f127a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart @@ -0,0 +1,271 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MentionLinkPreview extends StatefulWidget { + const MentionLinkPreview({ + super.key, + required this.linkInfo, + required this.onEnter, + required this.onExit, + required this.onCopyLink, + required this.onRemoveLink, + required this.onConvertTo, + required this.onOpenLink, + required this.triggerSize, + required this.showAtBottom, + }); + + final LinkInfo linkInfo; + final PointerEnterEventListener onEnter; + final PointerExitEventListener onExit; + final VoidCallback onCopyLink; + final VoidCallback onRemoveLink; + final VoidCallback onOpenLink; + final ValueChanged onConvertTo; + final Size triggerSize; + final bool showAtBottom; + + @override + State createState() => _MentionLinkPreviewState(); +} + +class _MentionLinkPreviewState extends State { + final menuController = PopoverController(); + bool isSelected = false; + + LinkInfo get linkInfo => widget.linkInfo; + + @override + void dispose() { + super.dispose(); + menuController.close(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context), + textColorScheme = theme.textColorScheme; + + final card = MouseRegion( + onEnter: widget.onEnter, + onExit: widget.onExit, + child: Container( + decoration: buildToolbarLinkDecoration(context, radius: 16), + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + child: FlowyNetworkImage( + url: linkInfo.imageUrl ?? '', + width: 280, + height: 120, + ), + ), + VSpace(12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyText.semibold( + linkInfo.siteName ?? '', + fontSize: 14, + figmaLineHeight: 20, + color: textColorScheme.primary, + overflow: TextOverflow.ellipsis, + ), + ), + VSpace(4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyText( + linkInfo.description ?? '', + fontSize: 12, + figmaLineHeight: 16, + color: textColorScheme.secondary, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + VSpace(36), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + height: 28, + child: Row( + children: [ + linkInfo.buildIconWidget(size: Size.square(16)), + HSpace(6), + Expanded( + child: FlowyText( + linkInfo.description ?? '', + fontSize: 12, + figmaLineHeight: 16, + color: textColorScheme.primary, + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w700, + ), + ), + buildMoreOptionButton(), + ], + ), + ), + VSpace(12), + ], + ), + ), + ); + + final clickPlaceHolder = MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: widget.onEnter, + onExit: widget.onExit, + child: GestureDetector( + child: Container( + height: 20, + width: widget.triggerSize.width, + color: Colors.white.withAlpha(1), + ), + onTap: () { + widget.onOpenLink.call(); + closePopover(); + }, + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: widget.showAtBottom + ? [clickPlaceHolder, card] + : [card, clickPlaceHolder], + ); + } + + Widget buildMoreOptionButton() { + return AppFlowyPopover( + controller: menuController, + direction: PopoverDirection.topWithLeftAligned, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + margin: EdgeInsets.zero, + borderRadius: BorderRadius.circular(12), + popupBuilder: (context) => buildConvertMenu(), + child: FlowyIconButton( + width: 28, + height: 28, + isSelected: isSelected, + hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + icon: FlowySvg( + FlowySvgs.toolbar_more_m, + size: Size.square(20), + ), + onPressed: () { + setState(() { + isSelected = true; + }); + showPopover(); + }, + ), + ); + } + + Widget buildConvertMenu() { + return MouseRegion( + onEnter: widget.onEnter, + onExit: widget.onExit, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(0.0), + children: + List.generate(MentionLinktMenuCommand.values.length, (index) { + final command = MentionLinktMenuCommand.values[index]; + return SizedBox( + height: 36, + child: FlowyButton( + text: FlowyText( + command.title, + fontWeight: FontWeight.w400, + figmaLineHeight: 20, + ), + onTap: () => onTap(command), + ), + ); + }), + ), + ), + ); + } + + void showPopover() { + keepEditorFocusNotifier.increase(); + menuController.show(); + } + + void closePopover() { + menuController.close(); + } + + void onTap(MentionLinktMenuCommand command) { + switch (command) { + case MentionLinktMenuCommand.toURL: + widget.onConvertTo(PasteMenuType.url); + break; + case MentionLinktMenuCommand.toBookmark: + widget.onConvertTo(PasteMenuType.bookmark); + break; + case MentionLinktMenuCommand.toEmbed: + widget.onConvertTo(PasteMenuType.embed); + break; + case MentionLinktMenuCommand.copyLink: + widget.onCopyLink(); + break; + case MentionLinktMenuCommand.removeLink: + widget.onRemoveLink(); + break; + } + closePopover(); + } +} + +enum MentionLinktMenuCommand { + toURL, + toBookmark, + toEmbed, + copyLink, + removeLink; + + String get title { + switch (this) { + case toURL: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl + .tr(); + case toBookmark: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_toBookmark + .tr(); + case toEmbed: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed + .tr(); + case copyLink: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink + .tr(); + case removeLink: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_removeLink + .tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart new file mode 100644 index 0000000000..7dcd21f423 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart @@ -0,0 +1,116 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +extension MenuExtension on EditorState { + MenuPosition? calculateMenuOffset({ + Rect? rect, + required double menuWidth, + required double menuHeight, + Offset menuOffset = const Offset(0, 10), + }) { + final selectionService = service.selectionService; + final selectionRects = selectionService.selectionRects; + late Rect startRect; + if (rect != null) { + startRect = rect; + } else { + if (selectionRects.isEmpty) return null; + startRect = selectionRects.first; + } + + final editorOffset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorHeight = renderBox!.size.height; + final editorWidth = renderBox!.size.width; + + // show below default + Alignment alignment = Alignment.topLeft; + final bottomRight = startRect.bottomRight; + final topRight = startRect.topRight; + var startOffset = bottomRight + menuOffset; + Offset offset = Offset( + startOffset.dx, + startOffset.dy, + ); + + // show above + if (startOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { + startOffset = topRight - menuOffset; + alignment = Alignment.bottomLeft; + + offset = Offset( + startOffset.dx, + editorHeight + editorOffset.dy - startOffset.dy, + ); + } + + // show on right + if (offset.dx + menuWidth < editorOffset.dx + editorWidth) { + offset = Offset( + offset.dx, + offset.dy, + ); + } else if (startOffset.dx - editorOffset.dx > menuWidth) { + // show on left + alignment = alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + offset = Offset( + editorWidth - offset.dx + editorOffset.dx, + offset.dy, + ); + } + return MenuPosition(align: alignment, offset: offset); + } +} + +class MenuPosition { + MenuPosition({ + required this.align, + required this.offset, + }); + + final Alignment align; + final Offset offset; + + LTRB get ltrb { + double? left, top, right, bottom; + switch (align) { + case Alignment.topLeft: + left = offset.dx; + top = offset.dy; + break; + case Alignment.bottomLeft: + left = offset.dx; + bottom = offset.dy; + break; + case Alignment.topRight: + right = offset.dx; + top = offset.dy; + break; + case Alignment.bottomRight: + right = offset.dx; + bottom = offset.dy; + break; + } + + return LTRB(left: left, top: top, right: right, bottom: bottom); + } +} + +class LTRB { + LTRB({this.left, this.top, this.right, this.bottom}); + + final double? left; + final double? top; + final double? right; + final double? bottom; + + Positioned buildPositioned({required Widget child}) => Positioned( + left: left, + top: top, + right: right, + bottom: bottom, + child: child, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 8b32ebdde9..cd332e14ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -58,6 +58,9 @@ class _HighlightColorPickerWidgetState @override Widget build(BuildContext context) { + if (editorState.selection == null) { + return const SizedBox.shrink(); + } final selectionRectList = editorState.selectionRects(); final top = selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index bd205ffea9..3664c9aee7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -591,10 +591,9 @@ class EditorStyleCustomizer { Node node, SelectableMixin delegate, ) { + if (UniversalPlatform.isMobile) return []; final delta = node.delta; - if (delta == null) { - return []; - } + if (delta == null) return []; final widgets = []; final textInserts = delta.whereType(); int index = 0; diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart index 26769ba7d3..3ab578b961 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart @@ -299,7 +299,8 @@ class _EmojiHandlerState extends State { .getTextInSelection( selection.copyWith( start: selection.start.copyWith(offset: startOffset), - end: selection.start.copyWith(offset: startOffset + _search.length + 1), + end: selection.start + .copyWith(offset: startOffset + _search.length + 1), ), ) .join(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index ed4c4d1623..0ef21267aa 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -225,12 +225,20 @@ class _ApplicationWidgetState extends State { Tooltip.dismissAllToolTips(); } }, - child: AppFlowyTheme( - data: Theme.of(context).brightness == Brightness.light - ? AppFlowyThemeData.light() - : AppFlowyThemeData.dark(), - child: MaterialApp.router( - builder: (context, child) => MediaQuery( + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + theme: state.lightTheme, + darkTheme: state.darkTheme, + themeMode: state.themeMode, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: state.locale, + routerConfig: routerConfig, + builder: (context, child) => AppFlowyTheme( + data: Theme.of(context).brightness == Brightness.light + ? AppFlowyThemeData.light() + : AppFlowyThemeData.dark(), + child: MediaQuery( // use the 1.0 as the textScaleFactor to avoid the text size // affected by the system setting. data: MediaQuery.of(context).copyWith( @@ -246,14 +254,6 @@ class _ApplicationWidgetState extends State { : child, ), ), - debugShowCheckedModeBanner: false, - theme: state.lightTheme, - darkTheme: state.darkTheme, - themeMode: state.themeMode, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: state.locale, - routerConfig: routerConfig, ), ), ), diff --git a/frontend/appflowy_flutter/lib/util/default_extensions.dart b/frontend/appflowy_flutter/lib/util/default_extensions.dart index 36bbdcb6b4..603a66d6cf 100644 --- a/frontend/appflowy_flutter/lib/util/default_extensions.dart +++ b/frontend/appflowy_flutter/lib/util/default_extensions.dart @@ -13,3 +13,11 @@ const List defaultImageExtensions = [ 'webp', 'bmp', ]; + +bool isNotImageUrl(String url) { + final nonImageSuffixRegex = RegExp( + r'(\.(io|html|php|json|txt|js|css|xml|md|log)(\?.*)?(#.*)?$)|/$', + caseSensitive: false, + ); + return nonImageSuffixRegex.hasMatch(url); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index d48c061c71..ed7e9aec29 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -151,7 +151,7 @@ class AppFlowyThemeBuilder { Brightness brightness, ) { return switch (brightness) { - Brightness.light => AppFlowyFillColorScheme( + Brightness.dark => AppFlowyFillColorScheme( primary: colorScheme.neutral.neutral100, primaryHover: colorScheme.neutral.neutral200, secondary: colorScheme.neutral.neutral300, @@ -196,7 +196,7 @@ class AppFlowyThemeBuilder { purpleThick: colorScheme.purple.purple500, purpleThickHover: colorScheme.purple.purple600, ), - Brightness.dark => AppFlowyFillColorScheme( + Brightness.light => AppFlowyFillColorScheme( primary: colorScheme.neutral.neutral1000, primaryHover: colorScheme.neutral.neutral900, secondary: colorScheme.neutral.neutral600, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart index 7fac9e07d0..b9136a00bc 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart @@ -13,6 +13,7 @@ class AFThemeExtensionV2 extends ThemeExtension { const AFThemeExtensionV2({ required this.icon_primary, required this.icon_tertiary, + required this.text_tertiary, required this.border_grey_quaternary, required this.fill_theme_select, required this.fill_grey_thick_alpha_1, @@ -21,6 +22,7 @@ class AFThemeExtensionV2 extends ThemeExtension { final Color icon_primary; final Color icon_tertiary; + final Color text_tertiary; final Color border_grey_quaternary; final Color fill_theme_select; final Color fill_grey_thick_alpha_1; @@ -30,6 +32,7 @@ class AFThemeExtensionV2 extends ThemeExtension { AFThemeExtensionV2 copyWith({ Color? icon_primary, Color? icon_tertiary, + Color? text_tertiary, Color? border_grey_quaternary, Color? fill_theme_select, Color? fill_grey_thick_alpha_1, @@ -38,6 +41,7 @@ class AFThemeExtensionV2 extends ThemeExtension { AFThemeExtensionV2( icon_primary: icon_primary ?? this.icon_primary, icon_tertiary: icon_tertiary ?? this.icon_tertiary, + text_tertiary: text_tertiary ?? this.text_tertiary, border_grey_quaternary: border_grey_quaternary ?? this.border_grey_quaternary, fill_theme_select: fill_theme_select ?? this.fill_theme_select, @@ -57,6 +61,8 @@ class AFThemeExtensionV2 extends ThemeExtension { Color.lerp(icon_primary, other.icon_primary, t) ?? icon_primary, icon_tertiary: Color.lerp(icon_tertiary, other.icon_tertiary, t) ?? icon_tertiary, + text_tertiary: + Color.lerp(text_tertiary, other.text_tertiary, t) ?? text_tertiary, border_grey_quaternary: Color.lerp(border_grey_quaternary, other.border_grey_quaternary, t) ?? border_grey_quaternary, @@ -75,6 +81,7 @@ class AFThemeExtensionV2 extends ThemeExtension { const AFThemeExtensionV2 darkAFThemeV2 = AFThemeExtensionV2( icon_primary: Color(0xFF1F2329), icon_tertiary: Color(0xFF99A1A8), + text_tertiary: Color(0xFFB5BBD3), border_grey_quaternary: Color(0xFFE8ECF3), fill_theme_select: Color(0x00BCF01F), fill_grey_thick_alpha_1: Color(0x1F23290F), @@ -84,6 +91,7 @@ const AFThemeExtensionV2 darkAFThemeV2 = AFThemeExtensionV2( const AFThemeExtensionV2 lightAFThemeV2 = AFThemeExtensionV2( icon_primary: Color(0xFF1F2329), icon_tertiary: Color(0xFF99A1A8), + text_tertiary: Color(0xFFB5BBD3), border_grey_quaternary: Color(0xFFE8ECF3), fill_theme_select: Color(0x00BCF01F), fill_grey_thick_alpha_1: Color(0x1F23290F), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index 8813cad09e..5b0b791c6c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -89,7 +89,6 @@ class _ManualTooltipState extends State { super.initState(); } - @override Widget build(BuildContext context) { return Tooltip( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1c6d66431d..ae44b95eb3 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -673,6 +673,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + favicon: + dependency: "direct main" + description: + name: favicon + sha256: ebb7423ba7ccc87d3cce9b60de1b5f72004de3cddeedd88d98463fc7f66faf0c + url: "https://pub.dev" + source: hosted + version: "1.1.2" ffi: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0cc553d1a7..de26c31c1e 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: auto_size_text_field: ^2.2.3 auto_updater: ^1.0.0 avatar_stack: ^3.0.0 + favicon: ^1.1.2 # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 diff --git a/frontend/resources/flowy_icons/20x/embed_fullscreen.svg b/frontend/resources/flowy_icons/20x/embed_fullscreen.svg new file mode 100644 index 0000000000..b8b197fb13 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/embed_fullscreen.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/turninto.svg b/frontend/resources/flowy_icons/20x/turninto.svg new file mode 100644 index 0000000000..598b870ec7 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/turninto.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/40x/embed_error.svg b/frontend/resources/flowy_icons/40x/embed_error.svg new file mode 100644 index 0000000000..68196c7b7e --- /dev/null +++ b/frontend/resources/flowy_icons/40x/embed_error.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e42660436c..29473d2e00 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2019,7 +2019,28 @@ "failedDuplicateFindView": "Failed to duplicate page - original view not found" } }, - "cannotMoveToItsChildren": "Cannot move to its children" + "cannotMoveToItsChildren": "Cannot move to its children", + "linkPreview": { + "typeSelection": { + "pasteAs": "Paste as", + "mention": "Mention", + "URL": "URL", + "bookmark": "Bookmark", + "embed": "Embed" + }, + "linkPreviewMenu": { + "toMetion": "Convert to Mention", + "toUrl": "Convert to URL", + "toEmbed": "Convert to Embed", + "toBookmark": "Convert to Bookmark", + "copyLink": "Copy Link", + "replace": "Replace", + "reload": "Reload", + "removeLink": "Remove Link", + "pasteHint": "Paste in https://...", + "refuseConnect": "refued to connect." + } + } }, "outlineBlock": { "placeholder": "Table of Contents" @@ -2513,6 +2534,7 @@ "copyLink": "Copy link", "removeLink": "Remove link", "editLink": "Edit link", + "convertTo": "Convert to", "linkText": "Text", "linkTextHint": "Please enter text", "linkAddressHint": "Please enter URL", From bcb1e8e4f5695f28bd22ff94278c267eaa33b8aa Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:57:41 +0800 Subject: [PATCH 300/384] chore: adjust replace, insert below, discard selection behavior (#7719) --- .../ai/operations/ai_writer_cubit.dart | 13 ++++++++++++- .../ai/widgets/ai_writer_scroll_wrapper.dart | 1 + .../editor_plugins/base/markdown_text_robot.dart | 10 +++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 7a885a3fe8..7b9da81d44 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -57,8 +57,13 @@ class AiWriterCubit extends Cubit { bool withDiscard = true, bool withUnformat = true, }) async { + if (aiWriterNode == null) { + return; + } if (withDiscard) { - await _textRobot.discard(); + await _textRobot.discard( + afterSelection: aiWriterNode!.aiWriterSelection, + ); } _textRobot.clear(); _textRobot.reset(); @@ -232,6 +237,12 @@ class AiWriterCubit extends Cubit { 'trigger accept action, markdown text: $trimmedMarkdownText', ); + await formatSelection( + editorState, + selection, + ApplySuggestionFormatType.clear, + ); + await _textRobot.deleteAINodes(); await _textRobot.replace( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart index 6b5f27e028..ef8ee81219 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart @@ -40,6 +40,7 @@ class _AiWriterScrollWrapperState extends State { onRemoveNode: () { aiWriterRegistered = false; widget.editorState.service.keyboardService?.enableShortcuts(); + widget.editorState.service.keyboardService?.enable(); }, onAppendToDocument: onAppendToDocument, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 031e157b33..429cd51221 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -118,7 +118,7 @@ class MarkdownTextRobot { } await _lock.synchronized(() async { - await _refresh(inMemoryUpdate: false); + await _refresh(inMemoryUpdate: false, updateSelection: true); }); if (_enableDebug) { @@ -156,7 +156,9 @@ class MarkdownTextRobot { } /// Discard the inserted content - Future discard() async { + Future discard({ + Selection? afterSelection, + }) async { final start = _insertPosition; if (start == null) { return; @@ -165,6 +167,8 @@ class MarkdownTextRobot { return; } + afterSelection ??= Selection.collapsed(start); + // fallback to the calculated position if the selection is null. final end = Position( path: start.path.nextNPath(_insertedNodes.length - 1), @@ -174,7 +178,7 @@ class MarkdownTextRobot { ); final transaction = editorState.transaction ..deleteNodes(deletedNodes) - ..afterSelection = Selection.collapsed(start); + ..afterSelection = afterSelection; await editorState.apply( transaction, From 351c891a5a82c73c1a2ac4fc5932f2b645971cea Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:21:02 +0800 Subject: [PATCH 301/384] fix: adjust ghost icon hover color (#7723) --- .../packages/appflowy_ui/lib/src/theme/data/builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index ed7e9aec29..c17dd14578 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -206,7 +206,7 @@ class AppFlowyThemeBuilder { quaternary: colorScheme.neutral.neutral100, quaternaryHover: colorScheme.neutral.neutral200, transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey10005, + primaryAlpha5: colorScheme.neutral.alphaGrey100005, primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010, primaryAlpha80: colorScheme.neutral.alphaGrey100080, primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, From 2e295e6891723aa087e44f06dddccb08e5336bfa Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 11 Apr 2025 14:25:19 +0800 Subject: [PATCH 302/384] fix: unable to accept the response from 'mark longer' (#7725) * fix: unable to accpet 'make it longer' * fix: markdown text robot test --- .../base/markdown_text_robot.dart | 41 ++++++++++-- .../text_robot/markdown_text_robot_test.dart | 64 +++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 429cd51221..0dea3d2864 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -324,6 +324,12 @@ class MarkdownTextRobot { required Selection selection, required String markdownText, }) async { + if (markdownText.isEmpty) { + assert(false, 'Expected non-empty markdown text'); + Log.error('Expected non-empty markdown text'); + return; + } + selection = selection.normalized; // If the selection is not a single node, do nothing. @@ -376,15 +382,42 @@ class MarkdownTextRobot { // } // ] final document = customMarkdownToDocument(markdownText); + final nodes = document.root.children; final decoder = DeltaMarkdownDecoder(); final markdownDelta = - document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText); + nodes.firstOrNull?.delta ?? decoder.convert(markdownText); + + if (markdownDelta.isEmpty) { + assert(false, 'Expected non-empty markdown delta'); + Log.error('Expected non-empty markdown delta'); + return; + } // Replace the delta of the selected node. final transaction = editorState.transaction; - transaction - ..deleteText(node, startIndex, length) - ..insertTextDelta(node, startIndex, markdownDelta); + + // it means the user selected the entire sentence, we just replace the node + if (startIndex == 0 && length == node.delta?.length) { + transaction + ..insertNodes(node.path.next, nodes) + ..deleteNode(node); + } else { + // it means the user selected a part of the sentence, we need to delete the + // selected part and insert the new delta. + transaction + ..deleteText(node, startIndex, length) + ..insertTextDelta(node, startIndex, markdownDelta); + + // Add the remaining nodes to the document. + final remainingNodes = nodes.skip(1); + if (remainingNodes.isNotEmpty) { + transaction.insertNodes( + node.path.next, + remainingNodes, + ); + } + } + await editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart index f2fcf8cd65..8b1b710f4e 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart @@ -487,6 +487,70 @@ void main() { ); expect(d7.attributes, null); }); + + test('replace markdown text with selection from start to end', () async { + final text1 = + '''The introduction of the World Wide Web in the early 1990s marked a turning point.'''; + final text2 = + '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption.'''; + final text3 = + '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; + + final document = Document( + root: pageNode( + children: [ + paragraphNode(delta: Delta()..insert(text1)), + paragraphNode(delta: Delta()..insert(text2)), + paragraphNode(delta: Delta()..insert(text3)), + ], + ), + ); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position(path: [0]), + end: Position(path: [0], offset: text1.length), + ); + + final markdownText = '''1. $text1 + +2. $text1 + +3. $text1'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final nodes = editorState.document.root.children; + expect(nodes.length, 5); + + final d1 = nodes[0].delta!.toList()[0] as TextInsert; + expect(d1.text, text1); + expect(d1.attributes, null); + expect(nodes[0].type, NumberedListBlockKeys.type); + + final d2 = nodes[1].delta!.toList()[0] as TextInsert; + expect(d2.text, text1); + expect(d2.attributes, null); + expect(nodes[1].type, NumberedListBlockKeys.type); + + final d3 = nodes[2].delta!.toList()[0] as TextInsert; + expect(d3.text, text1); + expect(d3.attributes, null); + expect(nodes[2].type, NumberedListBlockKeys.type); + + final d4 = nodes[3].delta!.toList()[0] as TextInsert; + expect(d4.text, text2); + expect(d4.attributes, null); + + final d5 = nodes[4].delta!.toList()[0] as TextInsert; + expect(d5.text, text3); + expect(d5.attributes, null); + }); }); group('markdown text robot - replace in multiple lines:', () { From 8c4324ee9d60b8a0fef7ed5cb5026cf881e0e7d6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Apr 2025 10:29:46 +0800 Subject: [PATCH 303/384] fix: otp launch review issues (#7730) * fix: add error text under text field * chore: update translation --- .../shared/common_operations.dart | 6 +- .../lib/user/application/sign_in_bloc.dart | 9 +- .../desktop_sign_in_screen.dart | 10 -- .../sign_in_screen/sign_in_screen.dart | 16 +- .../continue_with_email_and_password.dart | 109 +++++++------ ...inue_with_magic_link_or_passcode_page.dart | 148 +++++++++++------- .../continue_with_password_page.dart | 68 +++++--- .../widgets/sign_in_agreement.dart | 4 +- .../third_party_sign_in_buttons.dart | 1 + .../src/component/textfield/textfield.dart | 32 +++- frontend/resources/translations/en.json | 15 +- 11 files changed, 269 insertions(+), 149 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 4a117a71ff..d7a505d152 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -67,12 +67,10 @@ extension CommonOperations on WidgetTester { } else { // cloud version final anonymousButton = find.byType(SignInAnonymousButtonV2); - await tapButton(anonymousButton); + await tapButton(anonymousButton, warnIfMissed: true); } - if (Platform.isWindows) { - await pumpAndSettle(const Duration(milliseconds: 200)); - } + await pumpAndSettle(const Duration(milliseconds: 200)); } Future tapContinousAnotherWay() async { diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 54a60702c8..82269d1f1f 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -199,7 +199,9 @@ class SignInBloc extends Bloc { emit( result.fold( - (userProfile) => state.copyWith(isSubmitting: true), + (userProfile) => state.copyWith( + isSubmitting: false, + ), (error) => _stateFromCode(error), ), ); @@ -282,8 +284,9 @@ class SignInBloc extends Bloc { case ErrorCode.UserUnauthorized: final errorMsg = error.msg; String msg = LocaleKeys.signIn_generalError.tr(); - if (errorMsg.contains('rate limit')) { - msg = LocaleKeys.signIn_limitRateError.tr(); + if (errorMsg.contains('rate limit') || + errorMsg.contains('For security purposes')) { + msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); } else if (errorMsg.contains('invalid')) { msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 3a906ee0c4..65e8ee550b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -23,7 +23,6 @@ class DesktopSignInScreen extends StatelessWidget { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - const indicatorMinHeight = 4.0; return BlocBuilder( builder: (context, state) { return Scaffold( @@ -55,15 +54,6 @@ class DesktopSignInScreen extends StatelessWidget { // sign in agreement const SignInAgreement(), - // loading status - const VSpace(indicatorMinHeight), - state.isSubmitting - ? const LinearProgressIndicator( - minHeight: indicatorMinHeight, - ) - : const VSpace(indicatorMinHeight), - const VSpace(20), - const Spacer(), // anonymous sign in and settings diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index 5b99ad83f3..a428545d8d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -4,6 +4,7 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -37,10 +38,17 @@ class SignInScreen extends StatelessWidget { void _showSignInError(BuildContext context, SignInState state) { final successOrFail = state.successOrFail; if (successOrFail != null) { - handleUserProfileResult( - successOrFail, - context, - getIt(), + successOrFail.fold( + (userProfile) { + if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { + getIt().pushEncryptionScreen(context, userProfile); + } else { + getIt().goHomeScreen(context, userProfile); + } + }, + (error) { + handleOpenWorkspaceError(context, error); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index e918c3f4f1..a0dda40a36 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -2,14 +2,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; class ContinueWithEmailAndPassword extends StatefulWidget { const ContinueWithEmailAndPassword({super.key}); @@ -23,6 +21,7 @@ class _ContinueWithEmailAndPasswordState extends State { final controller = TextEditingController(); final focusNode = FocusNode(); + final emailKey = GlobalKey(); @override void dispose() { @@ -36,55 +35,74 @@ class _ContinueWithEmailAndPasswordState Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - return Column( - children: [ - SizedBox( - height: UniversalPlatform.isMobile ? 38.0 : 40.0, - child: AFTextField( + return BlocListener( + listener: (context, state) { + final successOrFail = state.successOrFail; + // only push the continue with magic link or passcode page if the magic link is sent successfully + if (successOrFail != null) { + successOrFail.fold( + (_) => emailKey.currentState?.clearError(), + (error) => emailKey.currentState?.syncError( + errorText: error.msg, + ), + ); + } else if (successOrFail == null && !state.isSubmitting) { + _pushContinueWithMagicLinkOrPasscodePage( + context, + controller.text, + ); + } + }, + child: Column( + children: [ + AFTextField( + key: emailKey, controller: controller, hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), radius: 10, - onSubmitted: (value) => _pushContinueWithMagicLinkOrPasscodePage( + onSubmitted: (value) => _signInWithEmail( context, value, ), ), - ), - VSpace(theme.spacing.l), - ContinueWithEmail( - onTap: () => _pushContinueWithMagicLinkOrPasscodePage( - context, - controller.text, + VSpace(theme.spacing.l), + ContinueWithEmail( + onTap: () => _signInWithEmail( + context, + controller.text, + ), ), - ), - // Hide password sign in until we implement the reset password / forgot password - // VSpace(theme.spacing.l), - // ContinueWithPassword( - // onTap: () => _pushContinueWithPasswordPage( - // context, - // controller.text, - // ), - // ), - ], + // VSpace(theme.spacing.l), + // ContinueWithPassword( + // onTap: () => _pushContinueWithPasswordPage( + // context, + // controller.text, + // ), + // ), + ], + ), ); } + void _signInWithEmail(BuildContext context, String email) { + if (!isEmail(email)) { + emailKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidEmail.tr(), + ); + return; + } + + context + .read() + .add(SignInEvent.signInWithMagicLink(email: email)); + } + void _pushContinueWithMagicLinkOrPasscodePage( BuildContext context, String email, ) { - if (!isEmail(email)) { - showToastNotification( - message: LocaleKeys.signIn_invalidEmail.tr(), - type: ToastificationType.error, - ); - return; - } - final signInBloc = context.read(); - signInBloc.add(SignInEvent.signInWithMagicLink(email: email)); - // push the a continue with magic link or passcode screen Navigator.push( context, @@ -114,18 +132,21 @@ class _ContinueWithEmailAndPasswordState // Navigator.push( // context, // MaterialPageRoute( - // builder: (context) => ContinueWithPasswordPage( - // email: email, - // backToLogin: () => Navigator.pop(context), - // onEnterPassword: (password) => signInBloc.add( - // SignInEvent.signInWithEmailAndPassword( - // email: email, - // password: password, + // builder: (context) => BlocProvider.value( + // value: signInBloc, + // child: ContinueWithPasswordPage( + // email: email, + // backToLogin: () => Navigator.pop(context), + // onEnterPassword: (password) => signInBloc.add( + // SignInEvent.signInWithEmailAndPassword( + // email: email, + // password: password, + // ), // ), + // onForgotPassword: () { + // // todo: implement forgot password + // }, // ), - // onForgotPassword: () { - // // todo: implement forgot password - // }, // ), // ), // ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index ea7ff087ff..5be30ef84c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -33,6 +33,8 @@ class _ContinueWithMagicLinkOrPasscodePageState ToastificationItem? toastificationItem; + final inputPasscodeKey = GlobalKey(); + @override void dispose() { passcodeController.dispose(); @@ -44,10 +46,13 @@ class _ContinueWithMagicLinkOrPasscodePageState Widget build(BuildContext context) { return BlocListener( listener: (context, state) { - if (state.isSubmitting) { - _showLoadingDialog(); - } else { - _dismissLoadingDialog(); + final successOrFail = state.successOrFail; + if (successOrFail != null && successOrFail.isFailure) { + successOrFail.onFailure((error) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(), + ); + }); } }, child: Scaffold( @@ -91,24 +96,39 @@ class _ContinueWithMagicLinkOrPasscodePageState return [ // Enter code manually - SizedBox( - height: 40, - child: AFTextField( - controller: passcodeController, - hintText: LocaleKeys.signIn_enterCode.tr(), - keyboardType: TextInputType.number, - radius: 10, - autoFocus: true, - onSubmitted: widget.onEnterPasscode, - ), + AFTextField( + key: inputPasscodeKey, + controller: passcodeController, + hintText: LocaleKeys.signIn_enterCode.tr(), + keyboardType: TextInputType.number, + radius: 10, + autoFocus: true, + onSubmitted: (passcode) { + if (passcode.isEmpty) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), + ); + } else { + widget.onEnterPasscode(passcode); + } + }, ), // todo: ask designer to provide the spacing VSpace(12), // continue to login AFFilledTextButton.primary( - text: 'Continue to sign in', - onTap: () => widget.onEnterPasscode(passcodeController.text), + text: LocaleKeys.signIn_continueToSignIn.tr(), + onTap: () { + final passcode = passcodeController.text; + if (passcode.isEmpty) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), + ); + } else { + widget.onEnterPasscode(passcode); + } + }, size: AFButtonSize.l, alignment: Alignment.center, ), @@ -123,6 +143,7 @@ class _ContinueWithMagicLinkOrPasscodePageState text: LocaleKeys.signIn_backToLogin.tr(), size: AFButtonSize.s, onTap: widget.backToLogin, + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (isHovering) { @@ -137,51 +158,70 @@ class _ContinueWithMagicLinkOrPasscodePageState List _buildLogoTitleAndDescription() { final theme = AppFlowyTheme.of(context); final spacing = VSpace(theme.spacing.xxl); - return [ - // logo - const AFLogo(), - spacing, + if (!isEnteringPasscode) { + return [ + // logo + const AFLogo(), + spacing, - // title - Text( - LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading.h3( - color: theme.textColorScheme.primary, + // title + Text( + LocaleKeys.signIn_checkYourEmail.tr(), + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), ), - ), - spacing, + spacing, - // description - Text( - LocaleKeys.signIn_temporaryVerificationSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, + // description + Text( + LocaleKeys.signIn_temporaryVerificationLinkSent.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, + Text( + widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } + spacing, + ]; + } else { + return [ + // logo + const AFLogo(), + spacing, - void _showLoadingDialog() { - _dismissLoadingDialog(); + // title + Text( + LocaleKeys.signIn_enterCode.tr(), + style: theme.textStyle.heading.h3( + color: theme.textColorScheme.primary, + ), + ), + spacing, - toastificationItem = showToastNotification( - message: LocaleKeys.signIn_signingIn.tr(), - ); - } - - void _dismissLoadingDialog() { - final toastificationItem = this.toastificationItem; - if (toastificationItem != null) { - toastification.dismiss(toastificationItem); + // description + Text( + LocaleKeys.signIn_temporaryVerificationCodeSent.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + Text( + widget.email, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + textAlign: TextAlign.center, + ), + spacing, + ]; } } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart index 3a281889ad..4ab40011d2 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart @@ -1,8 +1,10 @@ +import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ContinueWithPasswordPage extends StatefulWidget { const ContinueWithPasswordPage({ @@ -25,6 +27,7 @@ class ContinueWithPasswordPage extends StatefulWidget { class _ContinueWithPasswordPageState extends State { final passwordController = TextEditingController(); + final inputPasswordKey = GlobalKey(); @override void dispose() { @@ -38,18 +41,29 @@ class _ContinueWithPasswordPageState extends State { body: Center( child: SizedBox( width: 320, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo and title - ..._buildLogoAndTitle(), + child: BlocListener( + listener: (context, state) { + if (state.passwordError != null) { + inputPasswordKey.currentState?.syncError( + errorText: 'Incorrect password. Please try again.', + ); + } else { + inputPasswordKey.currentState?.clearError(); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo and title + ..._buildLogoAndTitle(), - // Password input and buttons - ..._buildPasswordSection(), + // Password input and buttons + ..._buildPasswordSection(), - // Back to login - ..._buildBackToLogin(), - ], + // Back to login + ..._buildBackToLogin(), + ], + ), ), ), ), @@ -100,25 +114,33 @@ class _ContinueWithPasswordPageState extends State { return [ // Password input AFTextField( + key: inputPasswordKey, controller: passwordController, hintText: 'Enter password', autoFocus: true, onSubmitted: widget.onEnterPassword, ), // todo: ask designer to provide the spacing - VSpace(12), + VSpace(8), - // todo: forgot password is not implemented yet // Forgot password button - // AFGhostTextButton( - // text: 'Forget password?', - // size: AFButtonSize.s, - // onTap: widget.onForgotPassword, - // textColor: (context, isHovering, disabled) { - // return theme.textColorScheme.theme; - // }, - // ), - VSpace(12), + Align( + alignment: Alignment.centerLeft, + child: AFGhostTextButton( + text: 'Forget password?', + size: AFButtonSize.s, + padding: EdgeInsets.zero, + onTap: widget.onForgotPassword, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } + return theme.textColorScheme.theme; + }, + ), + ), + VSpace(20), // Continue button AFFilledTextButton.primary( @@ -137,8 +159,12 @@ class _ContinueWithPasswordPageState extends State { text: 'Back to Login', size: AFButtonSize.s, onTap: widget.backToLogin, + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); + if (isHovering) { + return theme.fillColorScheme.themeThickHover; + } return theme.textColorScheme.theme; }, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index 4ea819d997..19a5ab9cf6 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -26,8 +26,8 @@ class SignInAgreement extends StatelessWidget { children: [ TextSpan( text: isLocalAuthEnabled - ? '${LocaleKeys.web_signInLocalAgreement.tr()} \n' - : '${LocaleKeys.web_signInAgreement.tr()} \n', + ? LocaleKeys.web_signInLocalAgreement.tr() + : LocaleKeys.web_signInAgreement.tr(), style: textStyle, ), TextSpan( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart index ed2e060c49..8d27846c46 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart @@ -101,6 +101,7 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { VSpace(theme.spacing.l), AFGhostTextButton( text: 'More options', + padding: EdgeInsets.zero, textColor: (context, isHovering, disabled) { if (isHovering) { return theme.fillColorScheme.themeThickHover; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 9fed31027f..595d4bb859 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -5,6 +5,11 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function( TextEditingController controller, ); +abstract class AFTextFieldState extends State { + void syncError({required String errorText}) {} + void clearError() {} +} + class AFTextField extends StatefulWidget { const AFTextField({ super.key, @@ -17,8 +22,12 @@ class AFTextField extends StatefulWidget { this.onChanged, this.onSubmitted, this.autoFocus, + this.height = 40.0, }); + /// The height of the text field. + final double height; + /// The hint text to display when the text field is empty. final String? hintText; @@ -52,7 +61,7 @@ class AFTextField extends StatefulWidget { State createState() => _AFTextFieldState(); } -class _AFTextFieldState extends State { +class _AFTextFieldState extends AFTextFieldState { late final TextEditingController effectiveController; bool hasError = false; @@ -146,8 +155,11 @@ class _AFTextFieldState extends State { ), ); + child = SizedBox(height: widget.height, child: child); + if (hasError && errorText.isNotEmpty) { child = Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ child, SizedBox(height: theme.spacing.xs), @@ -174,4 +186,22 @@ class _AFTextFieldState extends State { }); } } + + @override + void syncError({ + required String errorText, + }) { + setState(() { + hasError = true; + this.errorText = errorText; + }); + } + + @override + void clearError() { + setState(() { + hasError = false; + errorText = ''; + }); + } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 29473d2e00..91f9ce7743 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -48,7 +48,7 @@ "repeatPasswordEmptyError": "Repeat password can't be empty", "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", - "or": "OR", + "or": "or", "signInWithGoogle": "Continue with Google", "signInWithGithub": "Continue with GitHub", "signInWithDiscord": "Continue with Discord", @@ -70,15 +70,18 @@ "generalError": "Something went wrong. Please try again later", "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.", - "tokenHasExpiredOrInvalid": "The token has expired or is invalid. Please try again.", + "tokenHasExpiredOrInvalid": "The code has expired or is invalid. Please try again.", "signingIn": "Signing in...", "checkYourEmail": "Check your email", - "temporaryVerificationSent": "A temporary verification link has been sent. Please check your inbox at", + "temporaryVerificationLinkSent": "A temporary verification link has been sent.\nPlease check your inbox at", + "temporaryVerificationCodeSent": "A temporary verification code has been sent.\nPlease check your inbox at", "continueToSignIn": "Continue to sign in", "backToLogin": "Back to login", "enterCode": "Enter code", "enterCodeManually": "Enter code manually", - "continueWithEmail": "Continue with email" + "continueWithEmail": "Continue with email", + "invalidVerificationCode": "Please enter a valid verification code", + "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later." }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -2814,8 +2817,8 @@ "continueWithApple": "Continue with Apple ", "moreOptions": "More options", "collapse": "Collapse", - "signInAgreement": "By clicking \"Continue\" above, you agreed to AppFlowy's", - "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to AppFlowy's", + "signInAgreement": "By clicking \"Continue\" above, you agreed to \nAppFlowy's ", + "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to \nAppFlowy's ", "and": "and", "termOfUse": "Terms", "privacyPolicy": "Privacy Policy", From 4d172761ce76cd50805196093c4874ef68c0f1e1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 14 Apr 2025 10:36:42 +0800 Subject: [PATCH 304/384] chore: search with ai summary --- .../command_palette/folder_search_test.dart | 4 +- .../command_palette/command_palette_bloc.dart | 337 ++++++---- .../command_palette/search_listener.dart | 74 --- .../command_palette/search_result_ext.dart | 2 +- .../command_palette/search_service.dart | 93 ++- .../command_palette/command_palette.dart | 81 +-- .../command_palette/widgets/search_field.dart | 193 +++--- .../widgets/search_result_tile.dart | 175 +++--- .../widgets/search_results_list.dart | 114 +++- frontend/resources/translations/en.json | 1 + frontend/rust-lib/Cargo.lock | 334 +++++----- frontend/rust-lib/Cargo.toml | 12 +- frontend/rust-lib/flowy-ai/Cargo.toml | 3 - .../src/deps_resolve/cloud_service_impl.rs | 24 +- .../flowy-core/src/deps_resolve/user_deps.rs | 3 +- frontend/rust-lib/flowy-core/src/lib.rs | 4 +- frontend/rust-lib/flowy-error/Cargo.toml | 2 +- frontend/rust-lib/flowy-folder/src/manager.rs | 5 +- .../rust-lib/flowy-folder/src/manager_init.rs | 16 +- .../rust-lib/flowy-search-pub/src/cloud.rs | 11 +- .../rust-lib/flowy-search-pub/src/entities.rs | 21 +- frontend/rust-lib/flowy-search/Cargo.toml | 15 +- .../flowy-search/src/document/handler.rs | 187 ++++-- .../flowy-search/src/entities/notification.rs | 18 +- .../flowy-search/src/entities/query.rs | 14 +- .../flowy-search/src/entities/result.rs | 53 +- .../src/entities/search_filter.rs | 4 +- .../flowy-search/src/event_handler.rs | 9 +- .../flowy-search/src/folder/entities.rs | 4 +- .../flowy-search/src/folder/handler.rs | 43 +- .../flowy-search/src/folder/indexer.rs | 590 ++++++++---------- .../flowy-search/src/services/manager.rs | 125 ++-- .../rust-lib/flowy-search/src/services/mod.rs | 1 - .../flowy-search/src/services/notifier.rs | 61 -- .../flowy-server/src/af_cloud/impls/search.rs | 31 +- .../af_cloud/impls/user/cloud_service_impl.rs | 2 +- .../flowy-user-pub/src/workspace_service.rs | 2 +- .../src/services/authenticate_user.rs | 6 +- .../user_manager/manager_user_workspace.rs | 4 +- .../rust-lib/lib-infra/src/isolate_stream.rs | 1 + 40 files changed, 1445 insertions(+), 1234 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart delete mode 100644 frontend/rust-lib/flowy-search/src/services/notifier.rs diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart index 3271070c74..265d313320 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart @@ -49,7 +49,7 @@ void main() { // The score should be higher for "ViewOna" thus it should be shown first final secondDocumentWidget = tester .widget(find.byType(SearchResultTile).first) as SearchResultTile; - expect(secondDocumentWidget.result.data, secondDocument); + expect(secondDocumentWidget.item.data, secondDocument); // Change search to "ViewOne" await tester.enterText(searchFieldFinder, firstDocument); @@ -59,7 +59,7 @@ void main() { final firstDocumentWidget = tester.widget( find.byType(SearchResultTile).first, ) as SearchResultTile; - expect(firstDocumentWidget.result.data, firstDocument); + expect(firstDocumentWidget.item.data, firstDocument); }); testWidgets('Displaying icons in search results', (tester) async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index d952c09221..b01402c868 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -2,10 +2,8 @@ import 'dart:async'; import 'package:appflowy/plugins/trash/application/trash_listener.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/workspace/application/command_palette/search_listener.dart'; import 'package:appflowy/workspace/application/command_palette/search_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; @@ -13,184 +11,285 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'command_palette_bloc.freezed.dart'; -const _searchChannel = 'CommandPalette'; - class CommandPaletteBloc extends Bloc { CommandPaletteBloc() : super(CommandPaletteState.initial()) { - _searchListener.start( - onResultsChanged: _onResultsChanged, - ); + // Register event handlers + on<_SearchChanged>(_onSearchChanged); + on<_PerformSearch>(_onPerformSearch); + on<_NewSearchStream>(_onNewSearchStream); + on<_ResultsChanged>(_onResultsChanged); + on<_TrashChanged>(_onTrashChanged); + on<_WorkspaceChanged>(_onWorkspaceChanged); + on<_ClearSearch>(_onClearSearch); _initTrash(); - _dispatch(); } Timer? _debounceOnChanged; final TrashService _trashService = TrashService(); - final SearchListener _searchListener = SearchListener( - channel: _searchChannel, - ); final TrashListener _trashListener = TrashListener(); String? _oldQuery; String? _workspaceId; - int _messagesReceived = 0; @override Future close() { _trashListener.close(); - _searchListener.stop(); _debounceOnChanged?.cancel(); + state.searchResponseStream?.dispose(); return super.close(); } - void _dispatch() { - on((event, emit) async { - event.when( - searchChanged: _debounceOnSearchChanged, - trashChanged: (trash) async { - if (trash != null) { - return emit(state.copyWith(trash: trash)); - } - - final trashOrFailure = await _trashService.readTrash(); - final trashRes = trashOrFailure.fold( - (trash) => trash, - (error) => null, - ); - - if (trashRes != null) { - emit(state.copyWith(trash: trashRes.items)); - } - }, - performSearch: (search) async { - if (search.isNotEmpty && search != state.query) { - _oldQuery = state.query; - emit(state.copyWith(query: search, isLoading: true)); - await SearchBackendService.performSearch( - search, - workspaceId: _workspaceId, - channel: _searchChannel, - ); - } else { - emit(state.copyWith(query: null, isLoading: false, results: [])); - } - }, - resultsChanged: (results) { - if (state.query != _oldQuery) { - emit(state.copyWith(results: [], isLoading: true)); - _oldQuery = state.query; - _messagesReceived = 0; - } - - if (state.query != results.query) { - return; - } - - _messagesReceived++; - - emit( - state.copyWith( - results: _filterDuplicates(results.items), - isLoading: _messagesReceived != results.sends.toInt(), - ), - ); - }, - workspaceChanged: (workspaceId) { - _workspaceId = workspaceId; - emit(state.copyWith(results: [], query: '', isLoading: false)); - }, - clearSearch: () { - emit(state.copyWith(results: [], query: '', isLoading: false)); - }, - ); - }); - } - Future _initTrash() async { + // Start listening for trash updates _trashListener.start( trashUpdated: (trashOrFailed) { - final trash = trashOrFailed.toNullable(); - add(CommandPaletteEvent.trashChanged(trash: trash)); + add( + CommandPaletteEvent.trashChanged( + trash: trashOrFailed.toNullable(), + ), + ); }, ); + // Read initial trash state and forward results final trashOrFailure = await _trashService.readTrash(); - final trash = trashOrFailure.toNullable(); - - add(CommandPaletteEvent.trashChanged(trash: trash?.items)); - } - - void _debounceOnSearchChanged(String value) { - _debounceOnChanged?.cancel(); - _debounceOnChanged = Timer( - const Duration(milliseconds: 300), - () => _performSearch(value), + add( + CommandPaletteEvent.trashChanged( + trash: trashOrFailure.toNullable()?.items, + ), ); } - List _filterDuplicates(List results) { - final currentItems = [...state.results]; - final res = [...results]; - - for (final item in results) { - final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id); - if (duplicateIndex == -1) { - continue; - } - - final duplicate = currentItems[duplicateIndex]; - if (item.score < duplicate.score) { - res.remove(item); - } else { - currentItems.remove(duplicate); - } - } - - return res..addAll(currentItems); + FutureOr _onSearchChanged( + _SearchChanged event, + Emitter emit, + ) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer( + const Duration(milliseconds: 300), + () { + if (!isClosed) { + add(CommandPaletteEvent.performSearch(search: event.search)); + } + }, + ); } - void _performSearch(String value) => - add(CommandPaletteEvent.performSearch(search: value)); + FutureOr _onPerformSearch( + _PerformSearch event, + Emitter emit, + ) async { + if (event.search.isNotEmpty && event.search != state.query) { + _oldQuery = state.query; + emit(state.copyWith(query: event.search, isLoading: true)); - void _onResultsChanged(SearchResultNotificationPB results) => - add(CommandPaletteEvent.resultsChanged(results: results)); + // Fire off search asynchronously (fire and forget) + unawaited( + SearchBackendService.performSearch( + event.search, + workspaceId: _workspaceId, + ).then( + (result) => result.onSuccess((stream) { + if (!isClosed) { + add(CommandPaletteEvent.newSearchStream(stream: stream)); + } + }), + ), + ); + } else { + // Clear state if search is empty or unchanged + emit( + state.copyWith( + query: null, + isLoading: false, + resultItems: [], + resultSummaries: [], + ), + ); + } + } + + FutureOr _onNewSearchStream( + _NewSearchStream event, + Emitter emit, + ) { + state.searchResponseStream?.dispose(); + emit( + state.copyWith( + searchId: event.stream.searchId, + searchResponseStream: event.stream, + ), + ); + + event.stream.listen( + onItems: ( + List items, + String searchId, + bool isLoading, + ) { + if (_isActiveSearch(searchId)) { + add( + CommandPaletteEvent.resultsChanged( + items: items, + searchId: searchId, + isLoading: isLoading, + ), + ); + } + }, + onSummaries: ( + List summaries, + String searchId, + bool isLoading, + ) { + if (_isActiveSearch(searchId)) { + add( + CommandPaletteEvent.resultsChanged( + summaries: summaries, + searchId: searchId, + isLoading: isLoading, + ), + ); + } + }, + onFinished: (String searchId) { + if (_isActiveSearch(searchId)) { + add( + CommandPaletteEvent.resultsChanged( + searchId: searchId, + isLoading: false, + ), + ); + } + }, + ); + } + + FutureOr _onResultsChanged( + _ResultsChanged event, + Emitter emit, + ) async { + // If query was updated since last emission, clear previous results. + if (state.query != _oldQuery) { + emit( + state.copyWith( + resultItems: [], + resultSummaries: [], + isLoading: event.isLoading, + ), + ); + _oldQuery = state.query; + } + + // Check for outdated search streams + if (state.searchId != event.searchId) return; + + final updatedItems = + event.items ?? List.from(state.resultItems); + final updatedSummaries = + event.summaries ?? List.from(state.resultSummaries); + + emit( + state.copyWith( + resultItems: updatedItems, + resultSummaries: updatedSummaries, + isLoading: event.isLoading, + ), + ); + } + + // Update trash state and, in case of null, retry reading trash from the service + FutureOr _onTrashChanged( + _TrashChanged event, + Emitter emit, + ) async { + if (event.trash != null) { + emit(state.copyWith(trash: event.trash!)); + } else { + final trashOrFailure = await _trashService.readTrash(); + trashOrFailure.fold((trash) { + emit(state.copyWith(trash: trash.items)); + }, (error) { + // Optionally handle error; otherwise, we simply do nothing. + }); + } + } + + // Update the workspace and clear current search results and query + FutureOr _onWorkspaceChanged( + _WorkspaceChanged event, + Emitter emit, + ) { + _workspaceId = event.workspaceId; + emit( + state.copyWith( + query: '', + resultItems: [], + resultSummaries: [], + isLoading: false, + ), + ); + } + + // Clear search state + FutureOr _onClearSearch( + _ClearSearch event, + Emitter emit, + ) { + emit( + state.copyWith( + query: '', + resultItems: [], + resultSummaries: [], + isLoading: false, + searchId: null, + ), + ); + } + + bool _isActiveSearch(String searchId) => + !isClosed && state.searchId == searchId; } @freezed class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.searchChanged({required String search}) = _SearchChanged; - const factory CommandPaletteEvent.performSearch({required String search}) = _PerformSearch; - + const factory CommandPaletteEvent.newSearchStream({ + required SearchResponseStream stream, + }) = _NewSearchStream; const factory CommandPaletteEvent.resultsChanged({ - required SearchResultNotificationPB results, + required String searchId, + required bool isLoading, + List? items, + List? summaries, }) = _ResultsChanged; const factory CommandPaletteEvent.trashChanged({ @Default(null) List? trash, }) = _TrashChanged; - const factory CommandPaletteEvent.workspaceChanged({ @Default(null) String? workspaceId, }) = _WorkspaceChanged; - const factory CommandPaletteEvent.clearSearch() = _ClearSearch; } @freezed class CommandPaletteState with _$CommandPaletteState { const CommandPaletteState._(); - const factory CommandPaletteState({ @Default(null) String? query, - required List results, + @Default([]) List resultItems, + @Default([]) List resultSummaries, + @Default(null) SearchResponseStream? searchResponseStream, required bool isLoading, @Default([]) List trash, + @Default(null) String? searchId, }) = _CommandPaletteState; factory CommandPaletteState.initial() => - const CommandPaletteState(results: [], isLoading: false); + const CommandPaletteState(isLoading: false); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart deleted file mode 100644 index b22630eb74..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/search_notification.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; - -// Do not modify! -const _searchObjectId = "SEARCH_IDENTIFIER"; - -class SearchListener { - SearchListener({this.channel}); - - /// Use this to filter out search results from other channels. - /// - /// If null, it will receive search results from all - /// channels, otherwise it will only receive search results from the specified - /// channel. - /// - final String? channel; - - PublishNotifier? _updateNotifier = - PublishNotifier(); - PublishNotifier? _updateDidCloseNotifier = - PublishNotifier(); - SearchNotificationListener? _listener; - - void start({ - void Function(SearchResultNotificationPB)? onResultsChanged, - void Function(SearchResultNotificationPB)? onResultsClosed, - }) { - if (onResultsChanged != null) { - _updateNotifier?.addPublishListener(onResultsChanged); - } - - if (onResultsClosed != null) { - _updateDidCloseNotifier?.addPublishListener(onResultsClosed); - } - - _listener = SearchNotificationListener( - objectId: _searchObjectId, - handler: _handler, - channel: channel, - ); - } - - void _handler( - SearchNotification ty, - FlowyResult result, - ) { - switch (ty) { - case SearchNotification.DidUpdateResults: - result.fold( - (payload) => _updateNotifier?.value = - SearchResultNotificationPB.fromBuffer(payload), - (err) => Log.error(err), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _updateNotifier?.dispose(); - _updateNotifier = null; - _updateDidCloseNotifier?.dispose(); - _updateDidCloseNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart index 610c666667..563eac8a0a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -5,7 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -extension GetIcon on SearchResultPB { +extension GetIcon on SearchResponseItemPB { Widget? getIcon() { final iconValue = icon.value, iconType = icon.ty; if (iconType == ResultIconTypePB.Emoji) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart index 53a229ae66..6b862bc9ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -1,22 +1,107 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; +import 'dart:typed_data'; + import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/search_filter.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:nanoid/nanoid.dart'; +import 'package:fixnum/fixnum.dart'; class SearchBackendService { - static Future> performSearch( + static Future> performSearch( String keyword, { String? workspaceId, - String? channel, }) async { + final searchId = nanoid(6); + final stream = SearchResponseStream(searchId: searchId); + final filter = SearchFilterPB(workspaceId: workspaceId); final request = SearchQueryPB( search: keyword, filter: filter, - channel: channel, + searchId: searchId, + streamPort: Int64(stream.nativePort), ); - return SearchEventSearch(request).send(); + unawaited(SearchEventSearch(request).send()); + return FlowyResult.success(stream); + } +} + +class SearchResponseStream { + SearchResponseStream({required this.searchId}) { + _port.handler = _controller.add; + _subscription = _controller.stream.listen( + (Uint8List data) => _onResultsChanged(data), + ); + } + + final String searchId; + final RawReceivePort _port = RawReceivePort(); + final StreamController _controller = StreamController.broadcast(); + late StreamSubscription _subscription; + void Function( + List items, + String searchId, + bool isLoading, + )? _onItems; + void Function( + List summaries, + String searchId, + bool isLoading, + )? _onSummaries; + void Function(String searchId)? _onFinished; + int get nativePort => _port.sendPort.nativePort; + + Future dispose() async { + await _subscription.cancel(); + _port.close(); + } + + void _onResultsChanged(Uint8List data) { + final response = SearchResponsePB.fromBuffer(data); + + if (response.hasResult()) { + if (response.result.hasSearchResult()) { + _onItems?.call( + response.result.searchResult.items, + searchId, + response.isLoading, + ); + } + if (response.result.hasSearchSummary()) { + _onSummaries?.call( + response.result.searchSummary.items, + searchId, + response.isLoading, + ); + } + } else { + _onFinished?.call(searchId); + } + } + + void listen({ + required void Function( + List items, + String searchId, + bool isLoading, + )? onItems, + required void Function( + List summaries, + String searchId, + bool isLoading, + )? onSummaries, + required void Function(String searchId)? onFinished, + }) { + _onItems = onItems; + _onSummaries = onSummaries; + _onFinished = onFinished; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index 8eb7765c3a..9a507decc6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -4,7 +4,6 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -135,11 +134,15 @@ class CommandPaletteModal extends StatelessWidget { builder: (context, state) => FlowyDialog( alignment: Alignment.topCenter, insetPadding: const EdgeInsets.only(top: 100), - constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510), + constraints: const BoxConstraints( + maxHeight: 520, + maxWidth: 510, + minHeight: 420, + ), expandHeight: false, child: shortcutBuilder( + // Change mainAxisSize to max so Expanded works correctly. Column( - mainAxisSize: MainAxisSize.min, children: [ SearchField(query: state.query, isLoading: state.isLoading), if (state.query?.isEmpty ?? true) ...[ @@ -150,23 +153,26 @@ class CommandPaletteModal extends StatelessWidget { ), ), ], - if (state.results.isNotEmpty && + if (state.resultItems.isNotEmpty && (state.query?.isNotEmpty ?? false)) ...[ const Divider(height: 0), Flexible( - child: SearchResultsList( + child: SearchResultList( trash: state.trash, - results: state.results, + resultItems: state.resultItems, + resultSummaries: state.resultSummaries, ), ), - ] else if ((state.query?.isNotEmpty ?? false) && + ] + // When there are no results and the query is not empty and not loading, + // show the no results message, centered in the available space. + else if ((state.query?.isNotEmpty ?? false) && !state.isLoading) ...[ - const _NoResultsHint(), + const Divider(height: 0), + Expanded( + child: const _NoResultsHint(), + ), ], - _CommandPaletteFooter( - shouldShow: state.results.isNotEmpty && - (state.query?.isNotEmpty ?? false), - ), ], ), ), @@ -175,57 +181,16 @@ class CommandPaletteModal extends StatelessWidget { } } +/// Updated _NoResultsHint now centers its content. class _NoResultsHint extends StatelessWidget { const _NoResultsHint(); @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(height: 0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: FlowyText.regular( - LocaleKeys.commandPalette_noResultsHint.tr(), - textAlign: TextAlign.left, - ), - ), - ], - ); - } -} - -class _CommandPaletteFooter extends StatelessWidget { - const _CommandPaletteFooter({required this.shouldShow}); - - final bool shouldShow; - - @override - Widget build(BuildContext context) { - if (!shouldShow) { - return const SizedBox.shrink(); - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).lightGreyHover, - borderRadius: BorderRadius.circular(4), - ), - child: const FlowyText.semibold('TAB', fontSize: 10), - ), - const HSpace(4), - FlowyText(LocaleKeys.commandPalette_navigateHint.tr(), fontSize: 11), - ], + return Center( + child: FlowyText.regular( + LocaleKeys.commandPalette_noResultsHint.tr(), + textAlign: TextAlign.center, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart index c18024a909..1586ab0a7e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart @@ -7,7 +7,6 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -25,28 +24,31 @@ class SearchField extends StatefulWidget { class _SearchFieldState extends State { late final FocusNode focusNode; - late final controller = TextEditingController(text: widget.query); + late final TextEditingController controller; @override void initState() { super.initState(); - focusNode = FocusNode( - onKeyEvent: (node, event) { - if (node.hasFocus && - event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.arrowDown) { - node.nextFocus(); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - ); + controller = TextEditingController(text: widget.query); + focusNode = FocusNode(onKeyEvent: _handleKeyEvent); focusNode.requestFocus(); - controller.selection = TextSelection( - baseOffset: 0, - extentOffset: controller.text.length, - ); + // Update the text selection after the first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); + }); + } + + KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { + if (node.hasFocus && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + node.nextFocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } @override @@ -56,21 +58,83 @@ class _SearchFieldState extends State { super.dispose(); } + Widget _buildSuffixIcon(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, _) { + final hasText = value.text.trim().isNotEmpty; + final clearIcon = Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AFThemeExtension.of(context).lightGreyHover, + ), + child: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(16), + ), + ); + return AnimatedOpacity( + opacity: hasText ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: hasText + ? FlowyTooltip( + message: LocaleKeys.commandPalette_clearSearchTooltip.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: _clearSearch, + child: clearIcon, + ), + ), + ) + : clearIcon, + ); + }, + ); + } + @override Widget build(BuildContext context) { + // Cache theme and text styles + final theme = Theme.of(context); + final textStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 14); + final hintStyle = theme.textTheme.bodySmall?.copyWith( + fontSize: 14, + color: theme.hintColor, + ); + + // Choose the leading icon based on loading state + final Widget leadingIcon = widget.isLoading + ? FlowyTooltip( + message: LocaleKeys.commandPalette_loadingTooltip.tr(), + child: const SizedBox( + width: 20, + height: 20, + child: Padding( + padding: EdgeInsets.all(3.0), + child: CircularProgressIndicator(strokeWidth: 2.0), + ), + ), + ) + : SizedBox( + width: 20, + height: 20, + child: FlowySvg( + FlowySvgs.search_m, + color: theme.hintColor, + ), + ); + return Row( children: [ const HSpace(12), - FlowySvg( - FlowySvgs.search_m, - color: Theme.of(context).hintColor, - ), + leadingIcon, Expanded( child: FlowyTextField( focusNode: focusNode, controller: controller, - textStyle: - Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14), + textStyle: textStyle, decoration: InputDecoration( constraints: const BoxConstraints(maxHeight: 48), contentPadding: const EdgeInsets.symmetric(horizontal: 12), @@ -80,72 +144,14 @@ class _SearchFieldState extends State { ), isDense: false, hintText: LocaleKeys.commandPalette_placeholder.tr(), - hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 14, - color: Theme.of(context).hintColor, - ), - errorStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.error), + hintStyle: hintStyle, + errorStyle: theme.textTheme.bodySmall! + .copyWith(color: theme.colorScheme.error), suffix: Row( mainAxisSize: MainAxisSize.min, children: [ - AnimatedOpacity( - opacity: controller.text.trim().isNotEmpty ? 1 : 0, - duration: const Duration(milliseconds: 200), - child: Builder( - builder: (context) { - final icon = Container( - padding: const EdgeInsets.all(1), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AFThemeExtension.of(context).lightGreyHover, - ), - child: const FlowySvg( - FlowySvgs.close_s, - size: Size.square(16), - ), - ); - if (controller.text.isEmpty) { - return icon; - } - - return FlowyTooltip( - message: - LocaleKeys.commandPalette_clearSearchTooltip.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: controller.text.trim().isNotEmpty - ? _clearSearch - : null, - child: icon, - ), - ), - ); - }, - ), - ), + _buildSuffixIcon(context), const HSpace(8), - FlowyTooltip( - message: LocaleKeys.commandPalette_betaTooltip.tr(), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 5, - vertical: 2, - ), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).lightGreyHover, - borderRadius: BorderRadius.circular(4), - ), - child: FlowyText.semibold( - LocaleKeys.commandPalette_betaLabel.tr(), - fontSize: 11, - lineHeight: 1.2, - ), - ), - ), ], ), counterText: "", @@ -155,9 +161,7 @@ class _SearchFieldState extends State { ), errorBorder: OutlineInputBorder( borderRadius: Corners.s8Border, - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), + borderSide: BorderSide(color: theme.colorScheme.error), ), ), onChanged: (value) => context @@ -165,17 +169,6 @@ class _SearchFieldState extends State { .add(CommandPaletteEvent.searchChanged(search: value)), ), ), - if (widget.isLoading) ...[ - FlowyTooltip( - message: LocaleKeys.commandPalette_loadingTooltip.tr(), - child: const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2.5), - ), - ), - const HSpace(12), - ], ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart index 5f26f07597..a11721d1c3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -16,12 +16,12 @@ import 'package:flutter/services.dart'; class SearchResultTile extends StatefulWidget { const SearchResultTile({ super.key, - required this.result, + required this.item, required this.onSelected, this.isTrashed = false, }); - final SearchResultPB result; + final SearchResponseItemPB item; final VoidCallback onSelected; final bool isTrashed; @@ -31,7 +31,6 @@ class SearchResultTile extends StatefulWidget { class _SearchResultTileState extends State { bool _hasFocus = false; - final focusNode = FocusNode(); @override @@ -40,104 +39,132 @@ class _SearchResultTileState extends State { super.dispose(); } + /// Helper to handle the selection action. + void _handleSelection() { + widget.onSelected(); + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.item.viewId), + ), + ); + } + + /// Helper to clean up preview text. + String _cleanPreview(String preview) { + return preview.replaceAll('\n', ' ').trim(); + } + @override Widget build(BuildContext context) { - final title = widget.result.data.orDefault( + final title = widget.item.data.orDefault( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), ); - final icon = widget.result.getIcon(); - final cleanedPreview = _cleanPreview(widget.result.preview); + final icon = widget.item.getIcon(); + final cleanedPreview = _cleanPreview(widget.item.preview); + final hasPreview = cleanedPreview.isNotEmpty; + final trashHintText = + widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null; + + // Build the tile content based on preview availability. + Widget tileContent; + if (hasPreview) { + tileContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.isTrashed) + FlowyText( + trashHintText!, + color: AFThemeExtension.of(context) + .textColor + .withAlpha(175), + fontSize: 10, + ), + FlowyText( + title, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + const VSpace(4), + _DocumentPreview(preview: cleanedPreview), + ], + ); + } else { + tileContent = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.isTrashed) + FlowyText( + trashHintText!, + color: + AFThemeExtension.of(context).textColor.withAlpha(175), + fontSize: 10, + ), + FlowyText( + title, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { - widget.onSelected(); - - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: widget.result.viewId), - ), - ); - }, + onTap: _handleSelection, child: Focus( + focusNode: focusNode, onKeyEvent: (node, event) { - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - + if (event is! KeyDownEvent) return KeyEventResult.ignored; if (event.logicalKey == LogicalKeyboardKey.enter) { - widget.onSelected(); - - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: widget.result.viewId), - ), - ); + _handleSelection(); return KeyEventResult.handled; } - return KeyEventResult.ignored; }, onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), child: FlowyHover( isSelected: () => _hasFocus, style: HoverStyle( + borderRadius: BorderRadius.circular(8), hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), foregroundColorOnHover: AFThemeExtension.of(context).textColor, ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // page icon - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // if the result is trashed, show a hint - if (widget.isTrashed) ...[ - FlowyText( - LocaleKeys.commandPalette_fromTrashHint.tr(), - color: AFThemeExtension.of(context) - .textColor - .withAlpha(175), - fontSize: 10, - ), - ], - // page title - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - // content preview - if (cleanedPreview.isNotEmpty) ...[ - const VSpace(4), - _DocumentPreview(preview: cleanedPreview), - ], - ], + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: tileContent, ), ), ), ), ); } - - String _cleanPreview(String preview) { - return preview.replaceAll('\n', ' ').trim(); - } } class _DocumentPreview extends StatelessWidget { @@ -147,9 +174,9 @@ class _DocumentPreview extends StatelessWidget { @override Widget build(BuildContext context) { + // Combine the horizontal padding for clarity: return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16) + - const EdgeInsets.only(left: 14), + padding: const EdgeInsets.fromLTRB(30, 0, 16, 0), child: FlowyText.regular( preview, color: Theme.of(context).hintColor, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index ed9becf29e..b2ca24b68f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -7,41 +7,99 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -class SearchResultsList extends StatelessWidget { - const SearchResultsList({ - super.key, +class SearchResultList extends StatelessWidget { + const SearchResultList({ required this.trash, - required this.results, + required this.resultItems, + required this.resultSummaries, + super.key, }); final List trash; - final List results; + final List resultItems; + final List resultSummaries; + + Widget _buildSectionHeader(String title) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 8), + child: Opacity( + opacity: 0.6, + child: FlowyText(title, fontSize: 12), + ), + ); + + Widget _buildSummariesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader(LocaleKeys.commandPalette_aiSummary.tr()), + ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: resultSummaries.length, + separatorBuilder: (_, __) => const Divider(height: 0), + itemBuilder: (_, index) => SearchSummaryTile( + summary: resultSummaries[index], + ), + ), + ], + ); + } + + Widget _buildResultsSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + _buildSectionHeader(LocaleKeys.commandPalette_bestMatches.tr()), + ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: resultItems.length, + separatorBuilder: (_, __) => const Divider(height: 0), + itemBuilder: (_, index) { + final item = resultItems[index]; + return SearchResultTile( + item: item, + onSelected: () => FlowyOverlay.pop(context), + isTrashed: trash.any((t) => t.id == item.viewId), + ); + }, + ), + ], + ); + } @override Widget build(BuildContext context) { - return ListView.separated( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - separatorBuilder: (_, __) => const Divider(height: 0), - itemCount: results.length + 1, - itemBuilder: (_, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8) + - const EdgeInsets.only(left: 16), - child: FlowyText( - LocaleKeys.commandPalette_bestMatches.tr(), - ), - ); - } - - final result = results[index - 1]; - return SearchResultTile( - result: result, - onSelected: () => FlowyOverlay.pop(context), - isTrashed: trash.any((t) => t.id == result.viewId), - ); - }, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + if (resultSummaries.isNotEmpty) _buildSummariesSection(), + const VSpace(10), + if (resultItems.isNotEmpty) _buildResultsSection(context), + ], + ), + ); + } +} + +class SearchSummaryTile extends StatelessWidget { + const SearchSummaryTile({required this.summary, super.key}); + + final SearchSummaryPB summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyText( + summary.content, + maxLines: 10, + ), ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 29473d2e00..147902d799 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2693,6 +2693,7 @@ "commandPalette": { "placeholder": "Search or ask a question...", "bestMatches": "Best matches", + "aiSummary": "AI summary", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index fdf8c8348e..fd3ae1a6b9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "af-plugin", "anyhow", @@ -362,7 +362,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "anyhow", "futures-util", @@ -376,7 +376,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "anyhow", "cfg-if", @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "bytes", @@ -604,7 +604,7 @@ dependencies = [ "backoff", "base64 0.21.5", "bytes", - "derive_builder", + "derive_builder 0.12.0", "futures", "rand 0.8.5", "reqwest 0.11.27", @@ -632,9 +632,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -643,9 +643,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", @@ -887,6 +887,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c5f8abc69af414cbd6f2103bb668b91e584072f2105e4b38bed79b6ad0975f" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69edf39b6f321cb2699a93fc20c256adb839719c42676d03f7aa975e4e5581d" +dependencies = [ + "darling 0.20.11", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.94", +] + [[package]] name = "borsh" version = "1.5.1" @@ -1134,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "again", "anyhow", @@ -1189,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1202,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "futures-channel", "futures-util", @@ -1474,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "bincode", @@ -1496,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "async-trait", @@ -1761,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1817,12 +1842,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1841,15 +1866,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", + "strsim 0.11.1", "syn 2.0.94", ] @@ -1866,11 +1891,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.8", + "darling_core 0.20.11", "quote", "syn 2.0.94", ] @@ -1944,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "bincode", "bytes", @@ -2027,7 +2052,16 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ - "derive_builder_macro", + "derive_builder_macro 0.12.0", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro 0.20.2", ] [[package]] @@ -2042,16 +2076,38 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "derive_builder_macro" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ - "derive_builder_core", + "derive_builder_core 0.12.0", "syn 1.0.109", ] +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core 0.20.2", + "syn 2.0.94", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -2162,9 +2218,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "downcast-rs" -version = "1.2.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" [[package]] name = "dtoa" @@ -2484,7 +2540,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "log", - "md5", "notify", "pin-project", "protobuf", @@ -2501,8 +2556,6 @@ dependencies = [ "tracing-subscriber", "uuid", "validator 0.18.1", - "zip 2.2.0", - "zip-extensions", ] [[package]] @@ -2857,15 +2910,16 @@ dependencies = [ name = "flowy-search" version = "0.1.0" dependencies = [ + "allo-isolate", "async-stream", "bytes", "collab", "collab-folder", + "derive_builder 0.20.2", "flowy-codegen", "flowy-derive", "flowy-error", "flowy-folder", - "flowy-notification", "flowy-search-pub", "flowy-user", "futures", @@ -2874,11 +2928,11 @@ dependencies = [ "protobuf", "serde", "serde_json", - "strsim 0.11.0", + "strsim 0.11.1", "strum_macros 0.26.1", "tantivy", - "tempfile", "tokio", + "tokio-stream", "tracing", "uuid", ] @@ -3290,19 +3344,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3418,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3433,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "app-error", "jsonwebtoken", @@ -3527,12 +3568,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" - [[package]] name = "hermit-abi" version = "0.5.0" @@ -3802,6 +3837,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperloglogplus" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" +dependencies = [ + "serde", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -4054,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "bytes", @@ -4104,9 +4148,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -4121,7 +4162,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi", "libc", "windows-sys 0.59.0", ] @@ -4144,6 +4185,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -4416,20 +4466,6 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru" version = "0.12.3" @@ -4608,11 +4644,10 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "measure_time" -version = "0.8.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" dependencies = [ - "instant", "log", ] @@ -4876,16 +4911,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.2", - "libc", -] - [[package]] name = "object" version = "0.32.1" @@ -4903,12 +4928,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oneshot" -version = "0.1.6" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" -dependencies = [ - "loom", -] +checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" [[package]] name = "opaque-debug" @@ -4988,9 +5010,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" dependencies = [ "stable_deref_trait", ] @@ -5167,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5187,6 +5209,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5254,6 +5277,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6482,12 +6518,6 @@ dependencies = [ "parking_lot 0.12.1", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -6606,18 +6636,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -6754,7 +6784,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" dependencies = [ "anyhow", "app-error", @@ -6844,9 +6874,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "sketches-ddsketch" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" dependencies = [ "serde", ] @@ -6967,9 +6997,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -7093,7 +7123,7 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "windows 0.52.0", + "windows", ] [[package]] @@ -7146,14 +7176,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tantivy" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" +checksum = "b21ad8b222d71c57aa979353ed702f0bc6d97e66d368962cbded57fbd19eedd7" dependencies = [ "aho-corasick", "arc-swap", "base64 0.22.1", "bitpacking", + "bon", "byteorder", "census", "crc32fast", @@ -7163,20 +7194,20 @@ dependencies = [ "fnv", "fs4", "htmlescape", - "itertools 0.12.1", + "hyperloglogplus", + "itertools 0.14.0", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", - "num_cpus", "once_cell", "oneshot", "rayon", "regex", "rust-stemmers", - "rustc-hash 1.1.0", + "rustc-hash 2.1.0", "serde", "serde_json", "sketches-ddsketch", @@ -7189,7 +7220,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 1.0.64", + "thiserror 2.0.9", "time", "uuid", "winapi", @@ -7197,22 +7228,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" +checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" +checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344" dependencies = [ "downcast-rs", "fastdivide", - "itertools 0.12.1", + "itertools 0.14.0", "serde", "tantivy-bitpacker", "tantivy-common", @@ -7222,9 +7253,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" +checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f" dependencies = [ "async-trait", "byteorder", @@ -7246,19 +7277,23 @@ dependencies = [ [[package]] name = "tantivy-query-grammar" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" +checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a" dependencies = [ "nom", + "serde", + "serde_json", ] [[package]] name = "tantivy-sstable" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" +checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416" dependencies = [ + "futures-util", + "itertools 0.14.0", "tantivy-bitpacker", "tantivy-common", "tantivy-fst", @@ -7267,9 +7302,9 @@ dependencies = [ [[package]] name = "tantivy-stacker" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" +checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1" dependencies = [ "murmurhash32", "rand_distr", @@ -7278,9 +7313,9 @@ dependencies = [ [[package]] name = "tantivy-tokenizer-api" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" +checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d" dependencies = [ "serde", ] @@ -7303,14 +7338,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8213,7 +8249,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" dependencies = [ - "darling 0.20.8", + "darling 0.20.11", "once_cell", "proc-macro-error", "proc-macro2", @@ -8227,7 +8263,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" dependencies = [ - "darling 0.20.8", + "darling 0.20.11", "once_cell", "proc-macro-error2", "proc-macro2", @@ -8489,15 +8525,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows" version = "0.52.0" @@ -9008,15 +9035,6 @@ dependencies = [ "zstd 0.13.2", ] -[[package]] -name = "zip-extensions" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" -dependencies = [ - "zip 2.2.0", -] - [[package]] name = "zopfli" version = "0.8.1" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d3427ef99c..0a3cf409bb 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -97,14 +97,16 @@ validator = { version = "0.18", features = ["derive"] } tokio-util = "0.7.11" zip = "2.2.0" dashmap = "6.0.1" +derive_builder = "0.20.2" +tantivy = { version = "0.24.0" } # Please using the following command to update the revision id # Current directory: frontend # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" } [profile.dev] opt-level = 0 @@ -152,6 +154,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index a08eae7e92..a31d42da61 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -41,9 +41,6 @@ reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" base64 = "0.21.5" futures-util = "0.3.30" -md5 = "0.7.0" -zip = { workspace = true, features = ["deflate"] } -zip-extensions = "0.8.0" pin-project = "1.1.5" flowy-storage-pub = { workspace = true } collab-integrate.workspace = true diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 32cf633a6f..836bd6b32b 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,6 +1,6 @@ +use crate::server_layer::{Server, ServerProvider}; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; -use client_api::entity::search_dto::SearchDocumentResponseItem; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab::core::origin::{CollabClient, CollabOrigin}; @@ -10,6 +10,9 @@ use collab_entity::CollabType; use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; +use flowy_ai_pub::cloud::search_dto::{ + SearchDocumentResponseItem, SearchResult, SearchSummaryResult, +}; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, @@ -46,8 +49,6 @@ use tracing::log::error; use tracing::{debug, info}; use uuid::Uuid; -use crate::server_layer::{Server, ServerProvider}; - #[async_trait] impl StorageCloudService for ServerProvider { async fn get_object_url(&self, object_id: ObjectIdentity) -> Result { @@ -874,4 +875,21 @@ impl SearchCloudService for ServerProvider { None => Err(FlowyError::internal().with_context("SearchCloudService not found")), } } + + async fn generate_search_summary( + &self, + workspace_id: &Uuid, + query: String, + search_results: Vec, + ) -> Result { + let server = self.get_server()?; + match server.search_service() { + Some(search_service) => { + search_service + .generate_search_summary(workspace_id, query, search_results) + .await + }, + None => Err(FlowyError::internal().with_context("SearchCloudService not found")), + } + } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs index 1fd1d5211c..73c2844a23 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -82,12 +82,13 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl { Ok(()) } - fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { // The remove_indices_for_workspace should not block the deletion of the workspace // Log the error and continue if let Err(err) = self .folder_manager .remove_indices_for_workspace(workspace_id) + .await { info!("Error removing indices for workspace: {}", err); } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 79a889f86f..31b98da5e6 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -165,9 +165,9 @@ impl AppFlowyCore { collab_builder .set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&authenticate_user)))); - let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade( + let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade( &authenticate_user, - )))); + ))); let folder_manager = FolderDepsResolver::resolve( Arc::downgrade(&authenticate_user), diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 69897435e9..61a7422f17 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -33,7 +33,7 @@ collab-document = { workspace = true, optional = true } collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } client-api = { workspace = true, optional = true } -tantivy = { version = "0.22.0", optional = true } +tantivy = { workspace = true, optional = true } uuid.workspace = true [features] diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 180a5dc1f3..ea89def872 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -2051,10 +2051,11 @@ impl FolderManager { .collect() } - pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub async fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { self .folder_indexer - .remove_indices_for_workspace(*workspace_id)?; + .remove_indices_for_workspace(*workspace_id) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index d61de1f237..59eb3e21ee 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -115,8 +115,9 @@ impl FolderManager { let index_content_rx = folder.subscribe_index_content(); self .folder_indexer - .set_index_content_receiver(index_content_rx, *workspace_id); - self.handle_index_folder(*workspace_id, &folder); + .set_index_content_receiver(index_content_rx, *workspace_id) + .await; + self.handle_index_folder(*workspace_id, &folder).await; folder_state_rx }; @@ -137,6 +138,8 @@ impl FolderManager { Arc::downgrade(&self.user), ); + self.folder_indexer.initialize().await; + Ok(()) } @@ -166,7 +169,7 @@ impl FolderManager { Ok(folder) } - fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) { + async fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) { let mut index_all = true; let encoded_collab = self @@ -191,12 +194,11 @@ impl FolderManager { if index_all { let views = folder.get_all_views(); let folder_indexer = self.folder_indexer.clone(); + let _ = folder_indexer + .remove_indices_for_workspace(workspace_id) + .await; // We spawn a blocking task to index all views in the folder spawn_blocking(move || { - // We remove old indexes just in case - let _ = folder_indexer.remove_indices_for_workspace(workspace_id); - - // We index all views from the workspace folder_indexer.index_all_views(views, workspace_id); }); } diff --git a/frontend/rust-lib/flowy-search-pub/src/cloud.rs b/frontend/rust-lib/flowy-search-pub/src/cloud.rs index b62e226b92..8108cbed9a 100644 --- a/frontend/rust-lib/flowy-search-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-search-pub/src/cloud.rs @@ -1,4 +1,6 @@ -use client_api::entity::search_dto::SearchDocumentResponseItem; +pub use client_api::entity::search_dto::{ + SearchDocumentResponseItem, SearchResult, SearchSummaryResult, +}; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use uuid::Uuid; @@ -10,4 +12,11 @@ pub trait SearchCloudService: Send + Sync + 'static { workspace_id: &Uuid, query: String, ) -> Result, FlowyError>; + + async fn generate_search_summary( + &self, + workspace_id: &Uuid, + query: String, + search_results: Vec, + ) -> Result; } diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs index b7145e993f..fc4c19359c 100644 --- a/frontend/rust-lib/flowy-search-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -1,9 +1,9 @@ -use std::any::Any; use std::sync::Arc; use collab::core::collab::IndexContentReceiver; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout}; use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; use uuid::Uuid; pub struct IndexableData { @@ -26,19 +26,22 @@ impl IndexableData { } } +#[async_trait] pub trait IndexManager: Send + Sync { - fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid); - fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; - fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; - fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; - fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>; - fn is_indexed(&self) -> bool; - - fn as_any(&self) -> &dyn Any; + async fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid); + async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; + async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; + async fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; + async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>; + async fn is_indexed(&self) -> bool; } +#[async_trait] pub trait FolderIndexManager: IndexManager { + async fn initialize(&self); + fn index_all_views(&self, views: Vec>, workspace_id: Uuid); + fn index_view_changes( &self, views: Vec>, diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index dd313d1aea..a803ad894f 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -17,13 +17,10 @@ flowy-error = { workspace = true, features = [ "impl_from_tantivy", "impl_from_serde", ] } -flowy-notification.workspace = true flowy-user.workspace = true flowy-search-pub.workspace = true flowy-folder = { workspace = true } - bytes.workspace = true -futures.workspace = true lib-dispatch.workspace = true lib-infra = { workspace = true } protobuf.workspace = true @@ -31,18 +28,18 @@ serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] } tracing.workspace = true - -async-stream = "0.3.4" +derive_builder.workspace = true strsim = "0.11.0" strum_macros = "0.26.1" -tantivy = { version = "0.22.0" } +tantivy.workspace = true uuid.workspace = true +allo-isolate = { version = "^0.1", features = ["catch-unwind"] } +futures.workspace = true +tokio-stream.workspace = true +async-stream = "0.3.6" [build-dependencies] flowy-codegen.workspace = true -[dev-dependencies] -tempfile = "3.10.0" - [features] dart = ["flowy-codegen/dart"] diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index d447d27dfb..45bf5864c7 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,16 +1,22 @@ -use flowy_error::FlowyResult; -use flowy_folder::{manager::FolderManager, ViewLayout}; -use flowy_search_pub::cloud::SearchCloudService; -use lib_infra::async_trait::async_trait; -use std::str::FromStr; -use std::sync::Arc; -use tracing::{trace, warn}; -use uuid::Uuid; - +use crate::entities::{ + CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB, + SearchSourcePB, SearchSummaryPB, +}; use crate::{ - entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB}, + entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, services::manager::{SearchHandler, SearchType}, }; +use async_stream::stream; +use flowy_error::FlowyResult; +use flowy_folder::{manager::FolderManager, ViewLayout}; +use flowy_search_pub::cloud::{SearchCloudService, SearchResult}; +use lib_infra::async_trait::async_trait; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; +use tokio_stream::{self, Stream}; +use tracing::{trace, warn}; +use uuid::Uuid; pub struct DocumentSearchHandler { pub cloud_service: Arc, @@ -39,65 +45,122 @@ impl SearchHandler for DocumentSearchHandler { &self, query: String, filter: Option, - ) -> FlowyResult> { - let filter = match filter { - Some(filter) => filter, - None => return Ok(vec![]), - }; + ) -> Pin> + Send + 'static>> { + let cloud_service = self.cloud_service.clone(); + let folder_manager = self.folder_manager.clone(); - let workspace_id = match filter.workspace_id { - Some(workspace_id) => workspace_id, - None => return Ok(vec![]), - }; + Box::pin(stream! { + let filter = match filter { + Some(f) => f, + None => { + yield Ok(CreateSearchResultPBArgs::default().build().unwrap()); + return; + }, + }; - let workspace_id = Uuid::from_str(&workspace_id)?; - let results = self - .cloud_service - .document_search(&workspace_id, query) - .await?; - trace!("[Search] remote search results: {:?}", results); + let workspace_id = match Uuid::from_str(&filter.workspace_id) { + Ok(id) => id, + Err(e) => { + yield Err(e.into()); + return; + } + }; - // Grab all views from folder cache - // Notice that `get_all_view_pb` returns Views that don't include trashed and private views - let views = self.folder_manager.get_all_views_pb().await?; - let mut search_results: Vec = vec![]; + let views = match folder_manager.get_all_views_pb().await { + Ok(views) => views, + Err(e) => { + yield Err(e); + return; + }, + }; - for result in results { - if let Some(view) = views.iter().find(|v| v.id == result.object_id.to_string()) { - // If there is no View for the result, we don't add it to the results - // If possible we will extract the icon to display for the result - let icon: Option = match view.icon.clone() { - Some(view_icon) => Some(ResultIconPB::from(view_icon)), - None => { - let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); - Some(ResultIconPB { - ty: ResultIconTypePB::Icon, - value: view_layout_ty.to_string(), - }) - }, - }; + let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await { + Ok(items) => items, + Err(e) => { + yield Err(e); + return; + }, + }; - search_results.push(SearchResultPB { - index_type: IndexTypePB::Document, - view_id: result.object_id.to_string(), - id: result.object_id.to_string(), - data: view.name.clone(), - icon, - score: result.score, - workspace_id: result.workspace_id.to_string(), - preview: result.preview, - }); - } else { - warn!("No view found for search result: {:?}", result); + trace!("[Search] search results: {:?}", result_items); + let summary_input = result_items + .iter() + .map(|v| SearchResult { + object_id: v.object_id, + content: v.content.clone(), + }) + .collect::>(); + + let mut items: Vec = Vec::new(); + for item in result_items.iter() { + if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) { + let icon: Option = match view.icon.clone() { + Some(view_icon) => Some(ResultIconPB::from(view_icon)), + None => { + let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); + Some(ResultIconPB { + ty: ResultIconTypePB::Icon, + value: view_layout_ty.to_string(), + }) + }, + }; + + items.push(SearchResponseItemPB { + index_type: IndexTypePB::Document, + view_id: item.object_id.to_string(), + id: item.object_id.to_string(), + data: view.name.clone(), + icon, + score: item.score, + workspace_id: item.workspace_id.to_string(), + preview: item.preview.clone(), + }); + } else { + warn!("No view found for search result: {:?}", item); + } } - } - trace!("[Search] showing results: {:?}", search_results); - Ok(search_results) - } + let search_result = RepeatedSearchResponseItemPB { + items, + }; + yield Ok( + CreateSearchResultPBArgs::default() + .search_result(Some(search_result)) + .build() + .unwrap(), + ); - /// Ignore for [DocumentSearchHandler] - fn index_count(&self) -> u64 { - 0 + // Search summary generation. + match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await { + Ok(summary_result) => { + trace!("[Search] search summary: {:?}", summary_result); + let summaries: Vec = summary_result + .summaries + .into_iter() + .filter_map(|v| { + v.metadata.as_object().and_then(|object| { + let id = object.get("id")?.as_str()?.to_string(); + let source = object.get("source")?.as_str()?.to_string(); + let metadata = SearchSourcePB {id, source }; + Some(SearchSummaryPB { content: v.content, metadata: Some(metadata) }) + }) + }) + .collect(); + + let summary_result = RepeatedSearchSummaryPB { + items: summaries, + }; + yield Ok( + CreateSearchResultPBArgs::default() + .search_summary(Some(summary_result)) + .build() + .unwrap(), + ); + } + Err(e) => { + warn!("Failed to generate search summary: {:?}", e); + } + } + }) } } diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index a28ed2b5d8..0a51eb85d3 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -1,20 +1,16 @@ +use super::SearchResultPB; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use super::SearchResultPB; - #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchResultNotificationPB { - #[pb(index = 1)] - pub items: Vec, +pub struct SearchResponsePB { + #[pb(index = 1, one_of)] + pub result: Option, #[pb(index = 2)] - pub sends: u64, + pub search_id: String, - #[pb(index = 3, one_of)] - pub channel: Option, - - #[pb(index = 4)] - pub query: String, + #[pb(index = 3)] + pub is_loading: bool, } #[derive(ProtoBuf_Enum, Debug, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/query.rs b/frontend/rust-lib/flowy-search/src/entities/query.rs index 8ffbcf3d46..65c92ebed0 100644 --- a/frontend/rust-lib/flowy-search/src/entities/query.rs +++ b/frontend/rust-lib/flowy-search/src/entities/query.rs @@ -13,13 +13,9 @@ pub struct SearchQueryPB { #[pb(index = 3, one_of)] pub filter: Option, - /// Used to identify the channel of the search - /// - /// This can be used to have multiple search notification listeners in place. - /// It is up to the client to decide how to handle this. - /// - /// If not set, then no channel is used. - /// - #[pb(index = 4, one_of)] - pub channel: Option, + #[pb(index = 4)] + pub search_id: String, + + #[pb(index = 5)] + pub stream_port: i64, } diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index 0f5ea4dc23..d7d6db0820 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -1,17 +1,54 @@ +use super::IndexTypePB; use collab_folder::{IconType, ViewIcon}; +use derive_builder::Builder; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_folder::entities::ViewIconPB; -use super::IndexTypePB; +#[derive(Debug, Default, ProtoBuf, Builder, Clone)] +#[builder(name = "CreateSearchResultPBArgs")] +#[builder(pattern = "mutable")] +pub struct SearchResultPB { + #[pb(index = 1, one_of)] + #[builder(default)] + pub search_result: Option, -#[derive(Debug, Default, ProtoBuf, Clone)] -pub struct RepeatedSearchResultPB { - #[pb(index = 1)] - pub items: Vec, + #[pb(index = 2, one_of)] + #[builder(default)] + pub search_summary: Option, } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchResultPB { +pub struct RepeatedSearchSummaryPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct SearchSummaryPB { + #[pb(index = 1)] + pub content: String, + + #[pb(index = 2, one_of)] + pub metadata: Option, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct SearchSourcePB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub source: String, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct RepeatedSearchResponseItemPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct SearchResponseItemPB { #[pb(index = 1)] pub index_type: IndexTypePB, @@ -37,9 +74,9 @@ pub struct SearchResultPB { pub preview: Option, } -impl SearchResultPB { +impl SearchResponseItemPB { pub fn with_score(&self, score: f64) -> Self { - SearchResultPB { + SearchResponseItemPB { index_type: self.index_type.clone(), view_id: self.view_id.clone(), id: self.id.clone(), diff --git a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs index 33031b3b2c..2059971a0d 100644 --- a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs +++ b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs @@ -2,6 +2,6 @@ use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] pub struct SearchFilterPB { - #[pb(index = 1, one_of)] - pub workspace_id: Option, + #[pb(index = 1)] + pub workspace_id: String, } diff --git a/frontend/rust-lib/flowy-search/src/event_handler.rs b/frontend/rust-lib/flowy-search/src/event_handler.rs index de611a078f..d79a719f6f 100644 --- a/frontend/rust-lib/flowy-search/src/event_handler.rs +++ b/frontend/rust-lib/flowy-search/src/event_handler.rs @@ -21,7 +21,14 @@ pub(crate) async fn search_handler( ) -> Result<(), FlowyError> { let query = data.into_inner(); let manager = upgrade_manager(manager)?; - manager.perform_search(query.search, query.filter, query.channel); + manager + .perform_search( + query.search, + query.stream_port, + query.filter, + query.search_id, + ) + .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index b3837668b8..98f6c469ca 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB}; +use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB}; #[derive(Debug, Serialize, Deserialize)] pub struct FolderIndexData { @@ -11,7 +11,7 @@ pub struct FolderIndexData { pub workspace_id: String, } -impl From for SearchResultPB { +impl From for SearchResponseItemPB { fn from(data: FolderIndexData) -> Self { let icon = if data.icon.is_empty() { None diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs index f92e17cda1..975609a227 100644 --- a/frontend/rust-lib/flowy-search/src/folder/handler.rs +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -1,12 +1,14 @@ -use crate::{ - entities::{SearchFilterPB, SearchResultPB}, - services::manager::{SearchHandler, SearchType}, +use super::indexer::FolderIndexManagerImpl; +use crate::entities::{ + CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB, }; +use crate::services::manager::{SearchHandler, SearchType}; +use async_stream::stream; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; +use std::pin::Pin; use std::sync::Arc; - -use super::indexer::FolderIndexManagerImpl; +use tokio_stream::{self, Stream}; pub struct FolderSearchHandler { pub index_manager: Arc, @@ -28,19 +30,26 @@ impl SearchHandler for FolderSearchHandler { &self, query: String, filter: Option, - ) -> FlowyResult> { - let mut results = self.index_manager.search(query, filter.clone())?; - if let Some(filter) = filter { - if let Some(workspace_id) = filter.workspace_id { - // Filter results by workspace ID - results.retain(|result| result.workspace_id == workspace_id); - } - } + ) -> Pin> + Send + 'static>> { + let index_manager = self.index_manager.clone(); - Ok(results) - } + Box::pin(stream! { + // Perform search (if search() returns a Result) + let mut items = match index_manager.search(query).await { + Ok(items) => items, + Err(err) => { + yield Err(err); + return; + } + }; - fn index_count(&self) -> u64 { - self.index_manager.num_docs() + if let Some(filter) = filter { + items.retain(|result| result.workspace_id == filter.workspace_id); + } + + // Build the search result. + let search_result = RepeatedSearchResponseItemPB {items}; + yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap()) + }) } } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 17989aca29..5de540b0f2 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -1,14 +1,5 @@ -use std::{ - any::Any, - collections::HashMap, - fs, - ops::Deref, - path::Path, - sync::{Arc, Mutex, MutexGuard, Weak}, -}; - use crate::{ - entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB}, + entities::SearchResponseItemPB, folder::schema::{ FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, @@ -19,172 +10,88 @@ use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexCont use flowy_error::{FlowyError, FlowyResult}; use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; use flowy_user::services::authenticate_user::AuthenticateUser; +use std::sync::{Arc, Weak}; +use std::{collections::HashMap, fs}; use super::entities::FolderIndexData; +use crate::entities::ResultIconTypePB; +use lib_infra::async_trait::async_trait; use strsim::levenshtein; use tantivy::{ collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, Index, IndexReader, IndexWriter, TantivyDocument, Term, }; +use tokio::sync::RwLock; +use tracing::{error, info}; use uuid::Uuid; -#[derive(Clone)] -pub struct FolderIndexManagerImpl { - folder_schema: Option, - index: Option, - index_reader: Option, - index_writer: Option>>, +pub struct TantivyState { + pub index: Index, + pub folder_schema: FolderSchema, + pub index_reader: IndexReader, + pub index_writer: IndexWriter, } const FOLDER_INDEX_DIR: &str = "folder_index"; +#[derive(Clone)] +pub struct FolderIndexManagerImpl { + auth_user: Weak, + state: Arc>>, +} + impl FolderIndexManagerImpl { - pub fn new(auth_user: Option>) -> Self { - let auth_user = match auth_user { - Some(auth_user) => auth_user, - None => { - return FolderIndexManagerImpl::empty(); - }, - }; - - // AuthenticateUser is required to get the index path - let authenticate_user = auth_user.upgrade(); - - // Storage path is the users data path with an index directory - // Eg. /usr/flowy-data/indexes - let storage_path = match authenticate_user { - Some(auth_user) => auth_user.get_index_path(), - None => { - tracing::error!("FolderIndexManager: AuthenticateUser is not available"); - return FolderIndexManagerImpl::empty(); - }, - }; - - // We check if the `folder_index` directory exists, if not we create it - let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR)); - if !index_path.exists() { - let res = fs::create_dir_all(&index_path); - if let Err(e) = res { - tracing::error!( - "FolderIndexManager failed to create index directory: {:?}", - e - ); - return FolderIndexManagerImpl::empty(); - } - } - - // The folder schema is used to define the fields of the index along - // with how they are stored and if the field is indexed - let folder_schema = FolderSchema::new(); - - // We open the existing or newly created folder_index directory - // This is required by the Tantivy Index, as it will use it to store - // and read index data - let index = match MmapDirectory::open(index_path) { - // We open or create an index that takes the directory r/w and the schema. - Ok(dir) => match Index::open_or_create(dir, folder_schema.schema.clone()) { - Ok(index) => index, - Err(e) => { - tracing::error!("FolderIndexManager failed to open index: {:?}", e); - return FolderIndexManagerImpl::empty(); - }, - }, - Err(e) => { - tracing::error!("FolderIndexManager failed to open index directory: {:?}", e); - return FolderIndexManagerImpl::empty(); - }, - }; - - // We only need one IndexReader per index - let index_reader = index.reader(); - let index_writer = index.writer(50_000_000); - - let (index_reader, index_writer) = match (index_reader, index_writer) { - (Ok(reader), Ok(writer)) => (reader, writer), - _ => { - tracing::error!("FolderIndexManager failed to instantiate index writer and/or reader"); - return FolderIndexManagerImpl::empty(); - }, - }; - + pub fn new(auth_user: Weak) -> Self { Self { - folder_schema: Some(folder_schema), - index: Some(index), - index_reader: Some(index_reader), - index_writer: Some(Arc::new(Mutex::new(index_writer))), + auth_user, + state: Arc::new(RwLock::new(None)), } } - fn index_all(&self, indexes: Vec) -> Result<(), FlowyError> { - if indexes.is_empty() { - return Ok(()); + async fn with_writer(&self, f: F) -> FlowyResult + where + F: FnOnce(&mut IndexWriter, &FolderSchema) -> FlowyResult, + { + let mut lock = self.state.write().await; + if let Some(ref mut state) = *lock { + f(&mut state.index_writer, &state.folder_schema) + } else { + Err(FlowyError::internal().with_context("Index not initialized. Call initialize first")) + } + } + + /// Initializes the state using the workspace directory. + async fn initialize_with_workspace(&self) -> FlowyResult<()> { + let auth_user = self + .auth_user + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?; + + let index_path = auth_user.get_index_path()?.join(FOLDER_INDEX_DIR); + if !index_path.exists() { + fs::create_dir_all(&index_path).map_err(|e| { + error!("Failed to create folder index directory: {:?}", e); + FlowyError::internal().with_context("Failed to create folder index") + })?; } - let mut index_writer = self.get_index_writer()?; - let folder_schema = self.get_folder_schema()?; + info!("Folder indexer initialized at: {:?}", index_path); + let folder_schema = FolderSchema::new(); + let dir = MmapDirectory::open(index_path)?; + let index = Index::open_or_create(dir, folder_schema.schema.clone())?; + let index_reader = index.reader()?; + let index_writer = index.writer(50_000_000)?; - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; - let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; - let workspace_id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - - for data in indexes { - let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - - let _ = index_writer.add_document(doc![ - id_field => data.id.clone(), - title_field => data.data.clone(), - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - } - - index_writer.commit()?; + *self.state.write().await = Some(TantivyState { + index, + folder_schema, + index_reader, + index_writer, + }); Ok(()) } - pub fn num_docs(&self) -> u64 { - self - .index_reader - .clone() - .map(|reader| reader.searcher().num_docs()) - .unwrap_or(0) - } - - fn empty() -> Self { - Self { - folder_schema: None, - index: None, - index_reader: None, - index_writer: None, - } - } - - fn get_index_writer(&self) -> FlowyResult> { - match &self.index_writer { - Some(index_writer) => match index_writer.deref().lock() { - Ok(writer) => Ok(writer), - Err(e) => { - tracing::error!("FolderIndexManager failed to lock index writer: {:?}", e); - Err(FlowyError::folder_index_manager_unavailable()) - }, - }, - None => Err(FlowyError::folder_index_manager_unavailable()), - } - } - - fn get_folder_schema(&self) -> FlowyResult { - match &self.folder_schema { - Some(folder_schema) => Ok(folder_schema.clone()), - None => Err(FlowyError::folder_index_manager_unavailable()), - } - } - fn extract_icon( &self, view_icon: Option, @@ -200,100 +107,68 @@ impl FolderIndexManagerImpl { icon = Some(view_icon.value); } else { icon_ty = ResultIconTypePB::Icon.into(); - let layout_ty: i64 = view_layout.into(); + let layout_ty = view_layout as i64; icon = Some(layout_ty.to_string()); } - (icon, icon_ty) } - pub fn search( - &self, - query: String, - _filter: Option, - ) -> Result, FlowyError> { - let folder_schema = self.get_folder_schema()?; - - let (index, index_reader) = self - .index - .as_ref() - .zip(self.index_reader.as_ref()) - .ok_or_else(FlowyError::folder_index_manager_unavailable)?; - - let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - - let length = query.len(); - let distance: u8 = if length >= 2 { 2 } else { 1 }; - - let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]); - query_parser.set_field_fuzzy(title_field, true, distance, true); - let built_query = query_parser.parse_query(&query.clone())?; - - let searcher = index_reader.searcher(); - let mut search_results: Vec = vec![]; - let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; - for (_score, doc_address) in top_docs { - let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?; - - let mut content = HashMap::new(); - let named_doc = retrieved_doc.to_named_doc(&folder_schema.schema); - for (k, v) in named_doc.0 { - content.insert(k, v[0].clone()); - } - - if content.is_empty() { - continue; - } - - let s = serde_json::to_string(&content)?; - let result: SearchResultPB = serde_json::from_str::(&s)?.into(); - let score = self.score_result(&query, &result.data); - search_results.push(result.with_score(score)); - } - - Ok(search_results) - } - - // Score result by distance fn score_result(&self, query: &str, term: &str) -> f64 { let distance = levenshtein(query, term) as f64; 1.0 / (distance + 1.0) } - fn get_schema_fields(&self) -> Result<(Field, Field, Field, Field, Field), FlowyError> { - let folder_schema = match self.folder_schema.clone() { - Some(schema) => schema, - _ => return Err(FlowyError::folder_index_manager_unavailable()), - }; + /// Simple implementation to index all given data by spawning async tasks. + fn index_all(&self, data_vec: Vec) -> Result<(), FlowyError> { + for data in data_vec { + let indexer = self.clone(); + tokio::spawn(async move { + let _ = indexer.add_index(data).await; + }); + } + Ok(()) + } - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; - let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; - let workspace_id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + /// Searches the index using the given query string. + pub async fn search(&self, query: String) -> Result, FlowyError> { + let lock = self.state.read().await; + let state = lock + .as_ref() + .ok_or_else(FlowyError::folder_index_manager_unavailable)?; + let schema = &state.folder_schema; + let index = &state.index; + let reader = &state.index_reader; - Ok(( - id_field, - title_field, - icon_field, - icon_ty_field, - workspace_id_field, - )) + let title_field = schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let mut parser = QueryParser::for_index(index, vec![title_field]); + parser.set_field_fuzzy(title_field, true, 2, true); + + let built_query = parser.parse_query(&query)?; + let searcher = reader.searcher(); + let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; + + let mut results = Vec::new(); + for (_score, doc_address) in top_docs { + let doc: TantivyDocument = searcher.doc(doc_address)?; + let named_doc = doc.to_named_doc(&schema.schema); + let mut content = HashMap::new(); + for (k, v) in named_doc.0 { + content.insert(k, v[0].clone()); + } + if !content.is_empty() { + let s = serde_json::to_string(&content)?; + let result: SearchResponseItemPB = serde_json::from_str::(&s)?.into(); + results.push(result.with_score(self.score_result(&query, &result.data) as f64)); + } + } + + Ok(results) } } +#[async_trait] impl IndexManager for FolderIndexManagerImpl { - fn is_indexed(&self) -> bool { - self - .index_reader - .clone() - .map(|reader| reader.searcher().num_docs() > 0) - .unwrap_or(false) - } - - fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { + async fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { let indexer = self.clone(); let wid = workspace_id; tokio::spawn(async move { @@ -301,31 +176,35 @@ impl IndexManager for FolderIndexManagerImpl { match msg { IndexContent::Create(value) => match serde_json::from_value::(value) { Ok(view) => { - let _ = indexer.add_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }); + let _ = indexer + .add_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid, + }) + .await; }, - Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize (create): {:?}", err), }, IndexContent::Update(value) => match serde_json::from_value::(value) { Ok(view) => { - let _ = indexer.update_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }); + let _ = indexer + .update_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid, + }) + .await; }, - Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err), }, IndexContent::Delete(ids) => { - if let Err(e) = indexer.remove_indices(ids) { - tracing::error!("FolderIndexManager error deserialize: {:?}", e); + if let Err(e) = indexer.remove_indices(ids).await { + tracing::error!("FolderIndexManager error (delete): {:?}", e); } }, } @@ -333,100 +212,108 @@ impl IndexManager for FolderIndexManagerImpl { }); } - fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { - let mut index_writer = self.get_index_writer()?; - - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - self.get_schema_fields()?; - - let delete_term = Term::from_field_text(id_field, &data.id.clone()); - - // Remove old index - index_writer.delete_term(delete_term); - + async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - - // Add new index - let _ = index_writer.add_document(doc![ - id_field => data.id.clone(), - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - - index_writer.commit()?; - - Ok(()) - } - - fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError> { - let mut index_writer = self.get_index_writer()?; - - let folder_schema = self.get_folder_schema()?; - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - for id in ids { - let delete_term = Term::from_field_text(id_field, &id); - index_writer.delete_term(delete_term); - } - - index_writer.commit()?; - - Ok(()) - } - - fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { - let mut index_writer = self.get_index_writer()?; - - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - self.get_schema_fields()?; - - let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - - // Add new index - let _ = index_writer.add_document(doc![ - id_field => data.id, - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - - index_writer.commit()?; - - Ok(()) - } - - /// Removes all indexes that are related by workspace id. This is useful - /// for cleaning indexes when eg. removing/leaving a workspace. - /// - fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> { - let mut index_writer = self.get_index_writer()?; - - let folder_schema = self.get_folder_schema()?; - let id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - let delete_term = Term::from_field_text(id_field, &workspace_id.to_string()); - index_writer.delete_term(delete_term); - - index_writer.commit()?; - - Ok(()) - } - - fn as_any(&self) -> &dyn Any { self + .with_writer(|index_writer, folder_schema| { + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + get_schema_fields(folder_schema)?; + let _ = index_writer.add_document(doc![ + id_field => data.id, + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.to_string(), + ]); + index_writer.commit()?; + Ok(()) + }) + .await?; + + Ok(()) + } + + async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { + self + .with_writer(|index_writer, folder_schema| { + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + get_schema_fields(folder_schema)?; + let delete_term = Term::from_field_text(id_field, &data.id); + index_writer.delete_term(delete_term); + + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + let _ = index_writer.add_document(doc![ + id_field => data.id, + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.to_string(), + ]); + + index_writer.commit()?; + Ok(()) + }) + .await?; + + Ok(()) + } + + async fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError> { + self + .with_writer(|index_writer, folder_schema| { + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + for id in ids { + let delete_term = Term::from_field_text(id_field, &id); + index_writer.delete_term(delete_term); + } + + index_writer.commit()?; + Ok(()) + }) + .await?; + + Ok(()) + } + + async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> { + self + .with_writer(|index_writer, folder_schema| { + let id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + let delete_term = Term::from_field_text(id_field, &workspace_id.to_string()); + index_writer.delete_term(delete_term); + index_writer.commit()?; + Ok(()) + }) + .await?; + Ok(()) + } + + async fn is_indexed(&self) -> bool { + let lock = self.state.read().await; + if let Some(ref state) = *lock { + state.index_reader.searcher().num_docs() > 0 + } else { + false + } } } +#[async_trait] impl FolderIndexManager for FolderIndexManagerImpl { + async fn initialize(&self) { + if let Err(e) = self.initialize_with_workspace().await { + error!("Failed to initialize FolderIndexManager: {:?}", e); + } + } + fn index_all_views(&self, views: Vec>, workspace_id: Uuid) { let indexable_data = views .into_iter() .map(|view| IndexableData::from_view(view, workspace_id)) .collect(); - let _ = self.index_all(indexable_data); } @@ -440,23 +327,50 @@ impl FolderIndexManager for FolderIndexManagerImpl { for change in changes { match change { FolderViewChange::Inserted { view_id } => { - let view = views_iter.find(|view| view.id == view_id); - if let Some(view) = view { + if let Some(view) = views_iter.find(|view| view.id == view_id) { let indexable_data = IndexableData::from_view(view, workspace_id); - let _ = self.add_index(indexable_data); + let f = self.clone(); + tokio::spawn(async move { + let _ = f.add_index(indexable_data).await; + }); } }, FolderViewChange::Updated { view_id } => { - let view = views_iter.find(|view| view.id == view_id); - if let Some(view) = view { + if let Some(view) = views_iter.find(|view| view.id == view_id) { let indexable_data = IndexableData::from_view(view, workspace_id); - let _ = self.update_index(indexable_data); + let f = self.clone(); + tokio::spawn(async move { + let _ = f.update_index(indexable_data).await; + }); } }, FolderViewChange::Deleted { view_ids } => { - let _ = self.remove_indices(view_ids); + let f = self.clone(); + tokio::spawn(async move { + let _ = f.remove_indices(view_ids).await; + }); }, - }; + } } } } + +fn get_schema_fields( + folder_schema: &FolderSchema, +) -> Result<(Field, Field, Field, Field, Field), FlowyError> { + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + let workspace_id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + Ok(( + id_field, + title_field, + icon_field, + icon_ty_field, + workspace_id_field, + )) +} diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index 84659b3037..30c6783fb0 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -1,12 +1,13 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner}; -use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB}; +use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB}; +use allo_isolate::Isolate; use flowy_error::FlowyResult; - use lib_infra::async_trait::async_trait; -use tokio::sync::broadcast; +use lib_infra::isolate_stream::{IsolateSink, SinkExt}; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use tokio_stream::{self, Stream, StreamExt}; +use tracing::{error, trace}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum SearchType { @@ -19,15 +20,12 @@ pub trait SearchHandler: Send + Sync + 'static { /// returns the type of search this handler is responsible for fn search_type(&self) -> SearchType; - /// performs a search and returns the results + /// performs a search and returns a stream of results async fn perform_search( &self, query: String, filter: Option, - ) -> FlowyResult>; - - /// returns the number of indexed objects - fn index_count(&self) -> u64; + ) -> Pin> + Send + 'static>>; } /// The [SearchManager] is used to inject multiple [SearchHandler]'s @@ -36,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static { /// pub struct SearchManager { pub handlers: HashMap>, - notifier: SearchNotifier, + current_search: Arc>>, // Track current search } impl SearchManager { @@ -46,45 +44,88 @@ impl SearchManager { .map(|handler| (handler.search_type(), handler)) .collect(); - // Initialize Search Notifier - let (notifier, _) = broadcast::channel(100); - tokio::spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run()); - - Self { handlers, notifier } + Self { + handlers, + current_search: Arc::new(tokio::sync::Mutex::new(None)), + } } pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc> { self.handlers.get(&search_type) } - pub fn perform_search( + pub async fn perform_search( &self, query: String, + stream_port: i64, filter: Option, - channel: Option, + search_id: String, ) { - let max: usize = self.handlers.len(); - let handlers = self.handlers.clone(); - for (_, handler) in handlers { - let q = query.clone(); - let f = filter.clone(); - let ch = channel.clone(); - let notifier = self.notifier.clone(); - - tokio::spawn(async move { - let res = handler.perform_search(q.clone(), f).await; - - let items = res.unwrap_or_default(); - - let notification = SearchResultNotificationPB { - items, - sends: max as u64, - channel: ch, - query: q, - }; - - let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification)); - }); + // Cancel previous search by updating current_search + { + let mut current = self.current_search.lock().await; + *current = Some(search_id.clone()); } + + let handlers = self.handlers.clone(); + let sink = IsolateSink::new(Isolate::new(stream_port)); + let mut join_handles = vec![]; + let current_search = self.current_search.clone(); + + for (_, handler) in handlers { + let mut clone_sink = sink.clone(); + let query = query.clone(); + let filter = filter.clone(); + let search_id = search_id.clone(); + let current_search = current_search.clone(); + + let handle = tokio::spawn(async move { + let mut stream = handler.perform_search(query.clone(), filter).await; + while let Some(result) = stream.next().await { + if !is_current_search(¤t_search, &search_id).await { + trace!("[Search] search changed, cancel search: {}", query); + break; + } + + if let Ok(result) = result { + let resp = SearchResponsePB { + result: Some(result), + search_id: search_id.clone(), + is_loading: true, + }; + if let Ok::, _>(data) = resp.try_into() { + if let Err(err) = clone_sink.send(data).await { + error!("Failed to send search result: {}", err); + break; + } + } + } + } + + if !is_current_search(¤t_search, &search_id).await { + trace!("[Search] search changed, cancel search: {}", query); + return; + } + + let resp = SearchResponsePB { + result: None, + search_id: search_id.clone(), + is_loading: true, + }; + if let Ok::, _>(data) = resp.try_into() { + let _ = clone_sink.send(data).await; + } + }); + join_handles.push(handle); + } + futures::future::join_all(join_handles).await; } } + +async fn is_current_search( + current_search: &Arc>>, + search_id: &str, +) -> bool { + let current = current_search.lock().await; + current.as_ref().map_or(false, |id| id == search_id) +} diff --git a/frontend/rust-lib/flowy-search/src/services/mod.rs b/frontend/rust-lib/flowy-search/src/services/mod.rs index 2a417e6c62..ff8de9eb9a 100644 --- a/frontend/rust-lib/flowy-search/src/services/mod.rs +++ b/frontend/rust-lib/flowy-search/src/services/mod.rs @@ -1,2 +1 @@ pub mod manager; -pub mod notifier; diff --git a/frontend/rust-lib/flowy-search/src/services/notifier.rs b/frontend/rust-lib/flowy-search/src/services/notifier.rs deleted file mode 100644 index abbf5d4b0c..0000000000 --- a/frontend/rust-lib/flowy-search/src/services/notifier.rs +++ /dev/null @@ -1,61 +0,0 @@ -use async_stream::stream; -use flowy_notification::NotificationBuilder; -use futures::stream::StreamExt; -use tokio::sync::broadcast; - -use crate::entities::{SearchNotification, SearchResultNotificationPB}; - -const SEARCH_OBSERVABLE_SOURCE: &str = "Search"; -const SEARCH_ID: &str = "SEARCH_IDENTIFIER"; - -#[derive(Clone)] -pub enum SearchResultChanged { - SearchResultUpdate(SearchResultNotificationPB), -} - -pub type SearchNotifier = broadcast::Sender; - -pub(crate) struct SearchResultReceiverRunner( - pub(crate) Option>, -); - -impl SearchResultReceiverRunner { - pub(crate) async fn run(mut self) { - let mut receiver = self.0.take().expect("Only take once"); - let stream = stream! { - while let Ok(changed) = receiver.recv().await { - yield changed; - } - }; - stream - .for_each(|changed| async { - match changed { - SearchResultChanged::SearchResultUpdate(notification) => { - send_notification( - SEARCH_ID, - SearchNotification::DidUpdateResults, - notification.channel.clone(), - ) - .payload(notification) - .send(); - }, - } - }) - .await; - } -} - -#[tracing::instrument(level = "trace")] -pub fn send_notification( - id: &str, - ty: SearchNotification, - channel: Option, -) -> NotificationBuilder { - let observable_source = &format!( - "{}{}", - SEARCH_OBSERVABLE_SOURCE, - channel.unwrap_or_default() - ); - - NotificationBuilder::new(id, ty, observable_source) -} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs index 35fa6b153a..1ce0995144 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs @@ -1,19 +1,16 @@ -use client_api::entity::search_dto::SearchDocumentResponseItem; +use crate::af_cloud::AFServer; +use flowy_ai_pub::cloud::search_dto::{ + SearchDocumentResponseItem, SearchResult, SearchSummaryResult, +}; use flowy_error::FlowyError; use flowy_search_pub::cloud::SearchCloudService; use lib_infra::async_trait::async_trait; use uuid::Uuid; -use crate::af_cloud::AFServer; - pub(crate) struct AFCloudSearchCloudServiceImpl { pub inner: T, } -// The limit of what the score should be for results, used to -// filter out irrelevant results. -// https://community.openai.com/t/rule-of-thumb-cosine-similarity-thresholds/693670/5 -const SCORE_LIMIT: f64 = 0.3; const DEFAULT_PREVIEW: u32 = 80; #[async_trait] @@ -28,14 +25,22 @@ where ) -> Result, FlowyError> { let client = self.inner.try_get_client()?; let result = client - .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW) + .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW, None) .await?; - // Filter out irrelevant results - let result = result - .into_iter() - .filter(|r| r.score > SCORE_LIMIT) - .collect(); + Ok(result) + } + + async fn generate_search_summary( + &self, + workspace_id: &Uuid, + query: String, + search_results: Vec, + ) -> Result { + let client = self.inner.try_get_client()?; + let result = client + .generate_search_summary(workspace_id, &query, search_results) + .await?; Ok(result) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 3c79cd38ca..e59166fc37 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -127,7 +127,7 @@ where let try_get_client = self.server.try_get_client(); let client = try_get_client?; let response = client.sign_in_password(&email, &password).await?; - Ok(response) + Ok(response.gotrue_response) } async fn sign_in_with_magic_link( diff --git a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs index 122ce2e60f..84185d310f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs +++ b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs @@ -20,5 +20,5 @@ pub trait UserWorkspaceService: Send + Sync { ) -> FlowyResult<()>; /// Removes local indexes when a workspace is left/deleted - fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>; + async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 84d43fe4f2..ab9a35b483 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -93,9 +93,9 @@ impl AuthenticateUser { self.database.get_connection(uid) } - pub fn get_index_path(&self) -> PathBuf { - let uid = self.user_id().unwrap_or(0); - PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes") + pub fn get_index_path(&self) -> FlowyResult { + let uid = self.user_id()?; + Ok(PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes")) } pub fn get_user_data_dir(&self) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 3f3d90127d..496019cd2e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -279,6 +279,7 @@ impl UserManager { self .user_workspace_service .did_delete_workspace(workspace_id) + .await } #[instrument(level = "info", skip(self), err)] @@ -295,7 +296,8 @@ impl UserManager { self .user_workspace_service - .did_delete_workspace(workspace_id)?; + .did_delete_workspace(workspace_id) + .await?; Ok(()) } diff --git a/frontend/rust-lib/lib-infra/src/isolate_stream.rs b/frontend/rust-lib/lib-infra/src/isolate_stream.rs index 358214e985..cebc2b7d10 100644 --- a/frontend/rust-lib/lib-infra/src/isolate_stream.rs +++ b/frontend/rust-lib/lib-infra/src/isolate_stream.rs @@ -7,6 +7,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; #[pin_project] +#[derive(Clone, Debug)] pub struct IsolateSink { isolate: Isolate, } From 69b98cb323d49a69c3240775e9ce75d745d2c5bb Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:47:55 +0800 Subject: [PATCH 305/384] fix: open board row as page (#7735) --- .../board/presentation/board_page.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 386be9cc15..70d00bcd25 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -386,15 +386,15 @@ class _BoardContentState extends State<_BoardContent> { scrollManager: scrollManager, ), ), - cardBuilder: (context, column, columnItem) => + cardBuilder: (cardContext, column, columnItem) => MultiBlocProvider( key: ValueKey("board_card_${column.id}_${columnItem.id}"), providers: [ BlocProvider.value( - value: context.read(), + value: cardContext.read(), ), BlocProvider.value( - value: context.read(), + value: cardContext.read(), ), BlocProvider( create: (_) => ViewLockStatusBloc(view: widget.view) @@ -402,7 +402,7 @@ class _BoardContentState extends State<_BoardContent> { ), ], child: BlocBuilder( - builder: (context, state) { + builder: (lockStatusContext, state) { return IgnorePointer( ignoring: state.isLocked, child: _BoardCard( @@ -412,6 +412,13 @@ class _BoardContentState extends State<_BoardContent> { notifier: widget.focusScope, cellBuilder: cellBuilder, compactMode: compactMode, + onOpenCard: (rowMeta) => _openCard( + context: context, + databaseController: lockStatusContext + .read() + .databaseController, + rowMeta: rowMeta, + ), ), ); }, @@ -581,6 +588,7 @@ class _BoardCard extends StatefulWidget { required this.cellBuilder, required this.notifier, required this.compactMode, + required this.onOpenCard, }); final AppFlowyGroupData afGroupData; @@ -589,6 +597,7 @@ class _BoardCard extends StatefulWidget { final CardCellBuilder cellBuilder; final BoardFocusScope notifier; final bool compactMode; + final void Function(RowMetaPB) onOpenCard; @override State<_BoardCard> createState() => _BoardCardState(); @@ -698,10 +707,8 @@ class _BoardCardState extends State<_BoardCard> { groupingFieldId: widget.groupItem.fieldInfo.id, isEditing: _isEditing, cellBuilder: widget.cellBuilder, - onTap: (context) => _openCard( - context: context, - databaseController: databaseController, - rowMeta: context.read().rowController.rowMeta, + onTap: (context) => widget.onOpenCard( + context.read().rowController.rowMeta, ), onShiftTap: (_) { Focus.of(context).requestFocus(); From 54df6b2863ce451fc0bbfa3052f61273d8ef44a3 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 14 Apr 2025 14:14:05 +0800 Subject: [PATCH 306/384] fix: link_preview launch review issues (#7731) * fix: some link_preview launch review issues * fix: some UI issues * chore: pasting a link will not check whether it is an image * fix: copy link to block not supported well * fix: mention UI issues * feat: support get youtube channel info * chore: update translation * feat: add shadow in appflowy theme * chore: remove AFThemeExtensionV2 * fix: some UI issues --- .../document_copy_and_paste_test.dart | 16 +- .../document/document_link_preview_test.dart | 9 +- .../lib/core/helpers/url_launcher.dart | 22 +- .../presentation/editor_configuration.dart | 1 - .../document/presentation/editor_page.dart | 16 +- .../ai/ai_writer_toolbar_item.dart | 11 +- .../copy_and_paste/custom_paste_command.dart | 4 +- .../delta/text_delta_extension.dart | 7 + .../link/link_create_menu.dart | 25 +-- .../link_embed_block_component.dart | 124 ++++++----- .../link_embed/link_embed_menu.dart | 46 ++-- .../link_preview/custom_link_parser.dart | 70 ++++--- .../link_preview/custom_link_preview.dart | 130 ++++++------ .../custom_link_preview_block_component.dart | 198 +++++++++--------- .../default_selectable_mixin.dart | 77 +++++++ .../link_parsers/default_parser.dart | 88 ++++++++ .../link_parsers/youtube_parser.dart | 86 ++++++++ .../link_preview/link_preview_cache.dart | 25 --- .../link_preview/paste_as/paste_as_menu.dart | 40 ++-- .../mention/mention_link_block.dart | 36 +++- .../mention/mention_link_preview.dart | 49 +++-- .../presentation/editor_plugins/plugins.dart | 1 - .../custom_format_toolbar_items.dart | 5 +- .../custom_hightlight_color_toolbar_item.dart | 5 +- .../custom_link_toolbar_item.dart | 6 +- .../custom_text_align_toolbar_item.dart | 9 +- .../custom_text_color_toolbar_item.dart | 8 +- .../text_heading_toolbar_item.dart | 9 +- .../text_suggestions_toolbar_item.dart | 8 +- .../lib/shared/flowy_error_page.dart | 1 - .../startup/tasks/appflowy_cloud_task.dart | 1 - .../helpers/handle_open_workspace_error.dart | 2 - .../lib/util/share_log_files.dart | 3 - .../appearance/desktop_appearance.dart | 6 - .../appearance/mobile_appearance.dart | 7 - .../sidebar/workspace/sidebar_workspace.dart | 1 - .../pages/account/account_deletion.dart | 4 - .../pages/settings_manage_data_view.dart | 1 - .../lib/src/theme/data/builder.dart | 30 +++ .../appflowy_ui/lib/src/theme/data/data.dart | 13 +- .../lib/src/theme/shadow/shadow.dart | 8 + .../packages/flowy_infra/lib/language.dart | 2 +- .../flowy_infra/lib/theme_extension_v2.dart | 99 --------- frontend/appflowy_flutter/pubspec.lock | 8 - frontend/appflowy_flutter/pubspec.yaml | 1 - .../link_preview/link_preview_test.dart | 47 +++++ frontend/resources/translations/en.json | 2 +- frontend/resources/translations/zh-CN.json | 21 ++ 48 files changed, 838 insertions(+), 550 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart delete mode 100644 frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart create mode 100644 frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index ee82f01d3f..d1e34edcb5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -347,8 +347,9 @@ void main() { await tester.tapButton(menu); final convertToLinkButton = find.text( - LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl.tr(), - ); + LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl + .tr(), + ); expect(convertToLinkButton, findsOneWidget); await tester.tapButton(convertToLinkButton); }, @@ -384,7 +385,6 @@ void main() { expect(node.attributes[LinkPreviewBlockKeys.url], url); }); - await tester.simulateKeyEvent( LogicalKeyboardKey.keyZ, isControlPressed: @@ -483,16 +483,6 @@ void main() { }); }); - testWidgets('paste image url without extension', (tester) async { - const plainText = - '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.pasteContent(plainText: plainText, (editorState) { - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotEmpty); - }); - }); - const testMarkdownText = ''' # I'm h1 ## I'm h2 diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart index f74011ee2b..39f8bfd4f6 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart @@ -5,6 +5,7 @@ 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/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; @@ -397,7 +398,7 @@ void main() { Future hoverAndConvert( WidgetTester tester, - PasteMenuType command, + LinkEmbedConvertCommand command, ) async { final embed = find.byType(LinkEmbedBlockComponent); expect(embed, findsOneWidget); @@ -425,7 +426,7 @@ void main() { final link = avaliableLink; await preparePage(tester); await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, PasteMenuType.mention); + await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention); final node = tester.editor.getNodeAtPath([0]); checkMention(node, link); }); @@ -434,7 +435,7 @@ void main() { final link = avaliableLink; await preparePage(tester); await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, PasteMenuType.url); + await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL); final node = tester.editor.getNodeAtPath([0]); checkUrl(node, link); }); @@ -444,7 +445,7 @@ void main() { final link = avaliableLink; await preparePage(tester); await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, PasteMenuType.bookmark); + await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark); final node = tester.editor.getNodeAtPath([0]); checkBookmark(node, link); }); diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index fa61e7f5b8..0502e79604 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -45,16 +45,18 @@ Future afLaunchUri( } // try to launch the uri directly - bool result; - try { - result = await launcher.launchUrl( - uri, - mode: mode, - webOnlyWindowName: webOnlyWindowName, - ); - } on PlatformException catch (e) { - Log.error('Failed to open uri: $e'); - return false; + bool result = await launcher.canLaunchUrl(uri); + if (result) { + try { + result = await launcher.launchUrl( + uri, + mode: mode, + webOnlyWindowName: webOnlyWindowName, + ); + } on PlatformException catch (e) { + Log.error('Failed to open uri: $e'); + return false; + } } // if the uri is not a valid url, try to launch it with http scheme diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index b3d99ab84b..5e7eefc24e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -983,7 +983,6 @@ CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( return const EdgeInsets.symmetric(vertical: 10); }, ), - cache: LinkPreviewDataCache(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index c2c18e48eb..0ffb7de73a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -20,9 +20,9 @@ import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -352,7 +352,6 @@ class _AppFlowyEditorPageState extends State final isLocked = context.read()?.state.isLocked ?? false; - final themeV2 = AFThemeExtensionV2.of(context); final editor = Directionality( textDirection: textDirection, child: AppFlowyEditor( @@ -431,6 +430,7 @@ class _AppFlowyEditorPageState extends State ), ); } + final appTheme = AppFlowyTheme.of(context); return Center( child: BlocProvider.value( value: context.read(), @@ -443,15 +443,9 @@ class _AppFlowyEditorPageState extends State ), items: toolbarItems, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 24, - color: themeV2.shadow_medium, - ), - ], + borderRadius: BorderRadius.circular(appTheme.borderRadius.l), + color: appTheme.surfaceColorScheme.primary, + boxShadow: [appTheme.shadow.small], ), toolbarBuilder: (_, child, onDismiss, isMetricsChanged) => BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 35575fe85a..467a847c53 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -5,8 +5,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -119,7 +119,7 @@ class _AiWriterToolbarActionListState extends State { } Widget buildChild(BuildContext context) { - final themeV2 = AFThemeExtensionV2.of(context); + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; final child = FlowyIconButton( width: 48, height: 32, @@ -131,13 +131,13 @@ class _AiWriterToolbarActionListState extends State { FlowySvg( FlowySvgs.toolbar_ai_writer_m, size: Size.square(20), - color: themeV2.icon_primary, + color: iconScheme.primary, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: themeV2.icon_tertiary, + color: iconScheme.primary, ), ], ), @@ -180,6 +180,7 @@ class ImproveWritingButton extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); final child = FlowyIconButton( width: 36, height: 32, @@ -187,7 +188,7 @@ class ImproveWritingButton extends StatelessWidget { icon: FlowySvg( FlowySvgs.toolbar_ai_improve_writing_m, size: Size.square(20.0), - color: AFThemeExtensionV2.of(context).icon_primary, + color: theme.iconColorTheme.primary, ), onPressed: () { if (_isAIEnabled(editorState)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index dd3f7362a9..6399d3b11f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -186,7 +186,7 @@ Future _pasteAsLinkPreview( node.delta?.toPlainText().isNotEmpty == true) { return false; } - + if (!isMobile) return false; final bool isImageUrl; try { isImageUrl = await _isImageUrl(text); @@ -195,7 +195,7 @@ Future _pasteAsLinkPreview( return false; } - if (!isMobile && !isImageUrl) return false; + if (!isImageUrl) return false; // insert the text with link format final textTransaction = editorState.transaction diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart index 5beba66c32..905c033bda 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -27,9 +28,15 @@ extension TextDeltaExtension on Delta { if (op.text == MentionBlockKeys.mentionChar) { final mention = attributes?[MentionBlockKeys.mention]; final mentionPageId = mention?[MentionBlockKeys.pageId]; + final mentionType = mention?[MentionBlockKeys.type]; if (mentionPageId != null) { text += await getMentionPageName(mentionPageId); continue; + } else if (mentionType == MentionType.externalLink.name) { + final url = mention?[MentionBlockKeys.url] ?? ''; + final info = await LinkInfoCache.get(url); + text += info?.title ?? url; + continue; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index a6e1a78299..6213896feb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -307,17 +308,13 @@ void showLinkCreateMenu( ShapeDecoration buildToolbarLinkDecoration( BuildContext context, { double radius = 12.0, -}) => - ShapeDecoration( - color: Theme.of(context).cardColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius), - ), - shadows: [ - const BoxShadow( - color: LinkStyle.shadowMedium, - blurRadius: 24, - offset: Offset(0, 4), - ), - ], - ); +}) { + final theme = AppFlowyTheme.of(context); + return ShapeDecoration( + color: theme.surfaceColorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + ), + shadows: [theme.shadow.small], + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart index 6dc6501c03..baf9702a36 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart @@ -1,6 +1,9 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; @@ -37,11 +40,12 @@ class LinkEmbedBlockComponent extends BlockComponentStatefulWidget { }); @override - State createState() => + DefaultSelectableMixinState createState() => LinkEmbedBlockComponentState(); } -class LinkEmbedBlockComponentState extends State +class LinkEmbedBlockComponentState + extends DefaultSelectableMixinState with BlockComponentConfigurable { @override BlockComponentConfiguration get configuration => widget.configuration; @@ -49,11 +53,11 @@ class LinkEmbedBlockComponentState extends State @override Node get node => widget.node; - String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; + String get url => widget.node.attributes[LinkPreviewBlockKeys.url] ?? ''; - EmbedLoadingStatus status = EmbedLoadingStatus.loading; + LinkLoadingStatus status = LinkLoadingStatus.loading; final parser = LinkParser(); - LinkInfo linkInfo = LinkInfo(); + late LinkInfo linkInfo = LinkInfo(url: url); final showActionsNotifier = ValueNotifier(false); bool isMenuShowing = false, isHovering = false; @@ -62,13 +66,14 @@ class LinkEmbedBlockComponentState extends State void initState() { super.initState(); parser.addLinkInfoListener((v) { + final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); if (mounted) { setState(() { - if (v.isEmpty() && linkInfo.isEmpty()) { - status = EmbedLoadingStatus.error; - } else { + if (hasNewInfo) { linkInfo = v; - status = EmbedLoadingStatus.idle; + status = LinkLoadingStatus.idle; + } else if (!hasOldInfo) { + status = LinkLoadingStatus.error; } }); } @@ -98,7 +103,13 @@ class LinkEmbedBlockComponentState extends State }, child: buildChild(context), ); - result = Padding(padding: padding, child: result); + final parent = node.parent; + EdgeInsets newPadding = padding; + if (parent?.type == CalloutBlockKeys.type) { + newPadding = padding.copyWith(right: padding.right + 10); + } + + result = Padding(padding: newPadding, child: result); if (widget.showActions && widget.actionBuilder != null) { result = BlockComponentActionWrapper( @@ -115,7 +126,7 @@ class LinkEmbedBlockComponentState extends State fillSceme = theme.fillColorScheme, borderScheme = theme.borderColorScheme; Widget child; - final isIdle = status == EmbedLoadingStatus.idle; + final isIdle = status == LinkLoadingStatus.idle; if (isIdle) { child = buildContent(context); } else { @@ -123,6 +134,7 @@ class LinkEmbedBlockComponentState extends State } return Container( height: 450, + key: widgetKey, decoration: BoxDecoration( color: isIdle ? Theme.of(context).cardColor : fillSceme.tertiaryHover, borderRadius: BorderRadius.all(Radius.circular(16)), @@ -150,7 +162,7 @@ class LinkEmbedBlockComponentState extends State node: node, onReload: () { setState(() { - status = EmbedLoadingStatus.loading; + status = LinkLoadingStatus.loading; }); Future.delayed(const Duration(milliseconds: 200), () { if (mounted) parser.start(url); @@ -185,43 +197,51 @@ class LinkEmbedBlockComponentState extends State ), ), ), - Container( - height: 64, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - child: Row( - children: [ - SizedBox.square( - dimension: 40, - child: Center( - child: linkInfo.buildIconWidget(size: Size.square(32)), - ), - ), - HSpace(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - linkInfo.siteName ?? '', - color: textScheme.primary, - fontSize: 14, - figmaLineHeight: 20, - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => + afLaunchUrlString(url, addingHttpSchemeWhenFailed: true), + child: Container( + height: 64, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + child: Row( + children: [ + SizedBox.square( + dimension: 40, + child: Center( + child: linkInfo.buildIconWidget(size: Size.square(32)), ), - VSpace(4), - FlowyText.regular( - url, - color: textScheme.secondary, - fontSize: 12, - figmaLineHeight: 16, - overflow: TextOverflow.ellipsis, + ), + HSpace(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + linkInfo.siteName ?? '', + color: textScheme.primary, + fontSize: 14, + figmaLineHeight: 20, + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + ), + VSpace(4), + FlowyText.regular( + url, + color: textScheme.secondary, + fontSize: 12, + figmaLineHeight: 16, + overflow: TextOverflow.ellipsis, + ), + ], ), - ], - ), + ), + ], ), - ], + ), ), ), ], @@ -230,7 +250,7 @@ class LinkEmbedBlockComponentState extends State Widget buildErrorLoadingWidget(BuildContext context) { final theme = AppFlowyTheme.of(context), textSceme = theme.textColorScheme; - final isLoading = status == EmbedLoadingStatus.loading; + final isLoading = status == LinkLoadingStatus.loading; return isLoading ? Center( child: SizedBox.square( @@ -264,7 +284,7 @@ class LinkEmbedBlockComponentState extends State ), TextSpan( text: LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_refuseConnect + .document_plugins_linkPreview_linkPreviewMenu_unableToDisplay .tr(), style: TextStyle( color: textSceme.primary, @@ -281,6 +301,10 @@ class LinkEmbedBlockComponentState extends State ), ); } -} -enum EmbedLoadingStatus { loading, idle, error } + @override + Node get currentNode => node; + + @override + EdgeInsets get boxPadding => padding; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart index 8b9ac122a8..bddfcb9b54 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart @@ -3,7 +3,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/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -70,7 +69,7 @@ class _LinkEmbedMenuState extends State { return Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), color: fillScheme.primaryAlpha80, ), child: Row( @@ -90,18 +89,19 @@ class _LinkEmbedMenuState extends State { FlowySvgs.toolbar_link_m, color: iconScheme.tertiary, ), + radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), tooltipText: LocaleKeys.editor_copyLink.tr(), preferBelow: false, onPressed: () => copyLink(context), ), - buildTurnIntoBotton(), + buildconvertBotton(), buildMoreOptionBotton(), ], ), ); } - Widget buildTurnIntoBotton() { + Widget buildconvertBotton() { final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; return AppFlowyPopover( offset: Offset(0, 6), @@ -117,22 +117,22 @@ class _LinkEmbedMenuState extends State { turnintoMenuNum--; checkToHideMenu(); }, - popupBuilder: (context) => buildTurnIntoMenu(), + popupBuilder: (context) => buildConvertMenu(), child: FlowyIconButton( icon: FlowySvg( FlowySvgs.turninto_m, color: iconScheme.tertiary, ), - tooltipText: LocaleKeys.document_toolbar_turnInto.tr(), + radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), + tooltipText: LocaleKeys.editor_convertTo.tr(), preferBelow: false, onPressed: showTurnIntoMenu, ), ); } - Widget buildTurnIntoMenu() { - final types = - PasteMenuType.values.where((e) => e != PasteMenuType.embed).toList(); + Widget buildConvertMenu() { + final types = LinkEmbedConvertCommand.values; return Padding( padding: const EdgeInsets.all(8.0), child: SeparatedColumn( @@ -149,16 +149,16 @@ class _LinkEmbedMenuState extends State { figmaLineHeight: 20, ), onTap: () { - if (command == PasteMenuType.bookmark) { + if (command == LinkEmbedConvertCommand.toBookmark) { final transaction = editorState.transaction; transaction.updateNode(node, { LinkPreviewBlockKeys.url: url, LinkEmbedKeys.previewType: '', }); editorState.apply(transaction); - } else if (command == PasteMenuType.mention) { + } else if (command == LinkEmbedConvertCommand.toMention) { convertUrlPreviewNodeToMention(editorState, node); - } else if (command == PasteMenuType.url) { + } else if (command == LinkEmbedConvertCommand.toURL) { convertUrlPreviewNodeToLink(editorState, node); } }, @@ -192,6 +192,7 @@ class _LinkEmbedMenuState extends State { FlowySvgs.toolbar_more_m, color: iconScheme.tertiary, ), + radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), tooltipText: LocaleKeys.document_toolbar_moreOptions.tr(), preferBelow: false, onPressed: showMoreOptionMenu, @@ -330,3 +331,24 @@ enum LinkEmbedMenuCommand { } } } + +enum LinkEmbedConvertCommand { + toMention, + toURL, + toBookmark; + + String get title { + switch (this) { + case toMention: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion + .tr(); + case toURL: + return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl + .tr(); + case toBookmark: + return LocaleKeys + .document_plugins_linkPreview_linkPreviewMenu_toBookmark + .tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart index 0032d9cd33..1907f68d29 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart @@ -5,39 +5,39 @@ import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_svg.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:favicon/favicon.dart'; import 'package:flutter/material.dart'; -// ignore: depend_on_referenced_packages -import 'package:flutter_link_previewer/flutter_link_previewer.dart' hide Size; +import 'link_parsers/default_parser.dart'; +import 'link_parsers/youtube_parser.dart'; class LinkParser { - static final LinkInfoCache _cache = LinkInfoCache(); final Set> _listeners = >{}; + static final Map _hostToParsers = { + 'www.youtube.com': YoutubeParser(), + 'youtube.com': YoutubeParser(), + 'youtu.be': YoutubeParser(), + }; - Future start(String url) async { - final data = await _cache.get(url); + Future start(String url, {LinkInfoParser? parser}) async { + final uri = Uri.tryParse(LinkInfoParser.formatUrl(url)) ?? Uri.parse(url); + final data = await LinkInfoCache.get(uri); if (data != null) { refreshLinkInfo(data); } - await _getLinkInfo(url); + + final host = uri.host; + final currentParser = parser ?? _hostToParsers[host] ?? DefaultParser(); + await _getLinkInfo(uri, currentParser); } - Future _getLinkInfo(String url) async { + Future _getLinkInfo(Uri uri, LinkInfoParser parser) async { try { - final previewData = await getPreviewData(url); - final favicon = await FaviconFinder.getBest(url); - final linkInfo = LinkInfo( - siteName: previewData.title, - description: previewData.description, - imageUrl: previewData.image?.url, - faviconUrl: favicon?.url, - ); - if (!linkInfo.isEmpty()) await _cache.set(url, linkInfo); + final linkInfo = await parser.parse(uri) ?? LinkInfo(url: '$uri'); + if (!linkInfo.isEmpty()) await LinkInfoCache.set(uri, linkInfo); refreshLinkInfo(linkInfo); return linkInfo; } catch (e, s) { Log.error('get link info error: ', e, s); - refreshLinkInfo(LinkInfo()); + refreshLinkInfo(LinkInfo(url: '$uri')); return null; } } @@ -60,35 +60,45 @@ class LinkParser { class LinkInfo { factory LinkInfo.fromJson(Map json) => LinkInfo( siteName: json['siteName'], + url: json['url'] ?? '', + title: json['title'], description: json['description'], imageUrl: json['imageUrl'], faviconUrl: json['faviconUrl'], ); LinkInfo({ + required this.url, this.siteName, + this.title, this.description, this.imageUrl, this.faviconUrl, }); + final String url; final String? siteName; + final String? title; final String? description; final String? imageUrl; final String? faviconUrl; Map toJson() => { + 'url': url, 'siteName': siteName, + 'title': title, 'description': description, 'imageUrl': imageUrl, 'faviconUrl': faviconUrl, }; + @override + String toString() { + return 'LinkInfo{url: $url, siteName: $siteName, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl}'; + } + bool isEmpty() { - return siteName == null || - description == null || - imageUrl == null || - faviconUrl == null; + return title == null; } Widget buildIconWidget({Size size = const Size.square(20.0)}) { @@ -116,20 +126,26 @@ class LinkInfo { } class LinkInfoCache { - final _linkInfoPrefix = 'link_info'; + static const _linkInfoPrefix = 'link_info'; - Future get(String url) async { + static Future get(Uri uri) async { final option = await getIt().getWithFormat( - _linkInfoPrefix + url, + '$_linkInfoPrefix$uri', (value) => LinkInfo.fromJson(jsonDecode(value)), ); return option; } - Future set(String url, LinkInfo data) async { + static Future set(Uri uri, LinkInfo data) async { await getIt().set( - _linkInfoPrefix + url, + '$_linkInfoPrefix$uri', jsonEncode(data.toJson()), ); } } + +enum LinkLoadingStatus { + loading, + idle, + error, +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 0b6ad9fd7b..880a33b817 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -3,8 +3,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -14,6 +16,8 @@ import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; +import 'custom_link_parser.dart'; + class CustomLinkPreviewWidget extends StatelessWidget { const CustomLinkPreviewWidget({ super.key, @@ -23,7 +27,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { this.description, this.imageUrl, this.isHovering = false, - this.status = LinkPreviewStatus.loading, + this.status = LinkLoadingStatus.loading, }); final Node node; @@ -32,7 +36,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { final String? imageUrl; final String url; final bool isHovering; - final LinkPreviewStatus status; + final LinkLoadingStatus status; @override Widget build(BuildContext context) { @@ -46,6 +50,8 @@ class CustomLinkPreviewWidget extends StatelessWidget { .text .fontSize ?? 16.0; + final isInDarkCallout = node.parent?.type == CalloutBlockKeys.type && + !Theme.of(context).isLightMode; final (fontSize, width) = UniversalPlatform.isDesktopOrWeb ? (documentFontSize, 160.0) : (documentFontSize - 2, 120.0); @@ -53,7 +59,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( border: Border.all( - color: isHovering + color: isHovering || isInDarkCallout ? borderScheme.greyTertiaryHover : borderScheme.greyTertiary, ), @@ -67,43 +73,44 @@ class CustomLinkPreviewWidget extends StatelessWidget { buildImage(context), Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 60, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - buildLoadingOrErrorWidget(), - if (title != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText.medium( - title!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize, - color: textScheme.primary, - figmaLineHeight: 20, - ), + padding: const EdgeInsets.fromLTRB(20, 12, 58, 12), + child: status != LinkLoadingStatus.idle + ? buildLoadingOrErrorWidget() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (title != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText.medium( + title!, + overflow: TextOverflow.ellipsis, + fontSize: fontSize, + color: textScheme.primary, + figmaLineHeight: 20, + ), + ), + if (description != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: FlowyText( + description!, + overflow: TextOverflow.ellipsis, + fontSize: fontSize - 4, + figmaLineHeight: 16, + color: textScheme.primary, + ), + ), + FlowyText( + url.toString(), + overflow: TextOverflow.ellipsis, + color: textScheme.secondary, + fontSize: fontSize - 4, + figmaLineHeight: 16, + ), + ], ), - if (description != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: FlowyText( - description!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize - 4, - figmaLineHeight: 16, - color: textScheme.primary, - ), - ), - FlowyText( - url.toString(), - overflow: TextOverflow.ellipsis, - color: textScheme.secondary, - fontSize: fontSize - 4, - figmaLineHeight: 16, - ), - ], - ), ), ), ], @@ -112,9 +119,12 @@ class CustomLinkPreviewWidget extends StatelessWidget { ); if (UniversalPlatform.isDesktopOrWeb) { - return InkWell( - onTap: () => afLaunchUrlString(url), - child: child, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => afLaunchUrlString(url), + child: child, + ), ); } @@ -156,7 +166,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { iconScheme = theme.iconColorTheme; final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; Widget child; - if (imageUrl != null) { + if (imageUrl?.isNotEmpty ?? false) { child = FlowyNetworkImage( url: imageUrl!, width: width, @@ -184,26 +194,22 @@ class CustomLinkPreviewWidget extends StatelessWidget { } Widget buildLoadingOrErrorWidget() { - if (status == LinkPreviewStatus.loading) { - return Expanded( - child: const Center( - child: SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ), + if (status == LinkLoadingStatus.loading) { + return const Center( + child: SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), ), ); - } else if (status == LinkPreviewStatus.error) { - return Expanded( - child: const Center( - child: SizedBox( - height: 16, - width: 16, - child: Icon( - Icons.error_outline, - color: Colors.red, - ), + } else if (status == LinkLoadingStatus.error) { + return const Center( + child: SizedBox( + height: 16, + width: 16, + child: Icon( + Icons.error_outline, + color: Colors.red, ), ), ); @@ -211,5 +217,3 @@ class CustomLinkPreviewWidget extends StatelessWidget { return SizedBox.shrink(); } } - -enum LinkPreviewStatus { loading, error, idle } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart index 3772169331..3f2128db52 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart @@ -1,19 +1,19 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; import 'custom_link_preview.dart'; +import 'default_selectable_mixin.dart'; import 'link_preview_menu.dart'; class CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder { CustomLinkPreviewBlockComponentBuilder({ super.configuration, - this.cache, }); - final LinkPreviewDataCacheInterface? cache; - @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { final node = blockComponentContext.node; @@ -35,7 +35,6 @@ class CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder { configuration: configuration, showActions: showActions(node), actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - cache: cache, ); } @@ -51,18 +50,15 @@ class CustomLinkPreviewBlockComponent extends BlockComponentStatefulWidget { super.showActions, super.actionBuilder, super.configuration = const BlockComponentConfiguration(), - this.cache, }); - final LinkPreviewDataCacheInterface? cache; - @override - State createState() => + DefaultSelectableMixinState createState() => CustomLinkPreviewBlockComponentState(); } class CustomLinkPreviewBlockComponentState - extends State + extends DefaultSelectableMixinState with BlockComponentConfigurable { @override BlockComponentConfiguration get configuration => widget.configuration; @@ -72,8 +68,9 @@ class CustomLinkPreviewBlockComponentState String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; - late final LinkPreviewParser parser; - late Future future; + final parser = LinkParser(); + LinkLoadingStatus status = LinkLoadingStatus.loading; + late LinkInfo linkInfo = LinkInfo(url: url); final showActionsNotifier = ValueNotifier(false); bool isMenuShowing = false, isHovering = false; @@ -81,21 +78,26 @@ class CustomLinkPreviewBlockComponentState @override void initState() { super.initState(); - parser = LinkPreviewParser(url: url, cache: widget.cache); - future = parser.start(); + parser.addLinkInfoListener((v) { + final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); + if (mounted) { + setState(() { + if (hasNewInfo) { + linkInfo = v; + status = LinkLoadingStatus.idle; + } else if (!hasOldInfo) { + status = LinkLoadingStatus.error; + } + }); + } + }); + parser.start(url); } @override - void didUpdateWidget(CustomLinkPreviewBlockComponent oldWidget) { - super.didUpdateWidget(oldWidget); - final url = widget.node.attributes[LinkPreviewBlockKeys.url]!; - final oldUrl = oldWidget.node.attributes[LinkPreviewBlockKeys.url]!; - if (url != oldUrl) { - parser = LinkPreviewParser(url: url, cache: widget.cache); - setState(() { - future = parser.start(); - }); - } + void dispose() { + parser.dispose(); + super.dispose(); } @override @@ -114,91 +116,79 @@ class CustomLinkPreviewBlockComponentState }, hitTestBehavior: HitTestBehavior.opaque, opaque: false, - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: showActionsNotifier, builder: (context, showActions, child) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - Widget child; - - if (snapshot.connectionState != ConnectionState.done) { - child = CustomLinkPreviewWidget( - node: node, - url: url, - isHovering: showActions, - ); - } else { - final title = parser.getContent(LinkPreviewRegex.title); - final description = - parser.getContent(LinkPreviewRegex.description); - final image = parser.getContent(LinkPreviewRegex.image); - - if (title == null && description == null && image == null) { - child = CustomLinkPreviewWidget( - node: node, - url: url, - isHovering: showActions, - status: LinkPreviewStatus.error, - ); - } else { - child = CustomLinkPreviewWidget( - node: node, - url: url, - title: title, - description: description, - imageUrl: image, - isHovering: showActions, - status: LinkPreviewStatus.idle, - ); - } - } - - child = Padding(padding: padding, child: child); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - child: child, - ); - } - - child = Stack( - children: [ - child, - if (showActions) - Positioned( - top: 16, - right: 16, - child: CustomLinkPreviewMenu( - onMenuShowed: () { - isMenuShowing = true; - }, - onMenuHided: () { - isMenuShowing = false; - if (!isHovering && mounted) { - showActionsNotifier.value = false; - } - }, - onReload: () { - if (mounted) { - setState(() { - future = parser.start(); - }); - } - }, - node: node, - ), - ), - ], - ); - - return child; - }, - ); + return buildPreview(showActions); }, ), ); } + + Widget buildPreview(bool showActions) { + Widget child = CustomLinkPreviewWidget( + key: widgetKey, + node: node, + url: url, + isHovering: showActions, + title: linkInfo.siteName, + description: linkInfo.description, + imageUrl: linkInfo.imageUrl, + status: status, + ); + + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + child = Stack( + children: [ + child, + if (showActions) + Positioned( + top: 12, + right: 12, + child: CustomLinkPreviewMenu( + onMenuShowed: () { + isMenuShowing = true; + }, + onMenuHided: () { + isMenuShowing = false; + if (!isHovering && mounted) { + showActionsNotifier.value = false; + } + }, + onReload: () { + setState(() { + status = LinkLoadingStatus.loading; + }); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) parser.start(url); + }); + }, + node: node, + ), + ), + ], + ); + + final parent = node.parent; + EdgeInsets newPadding = padding; + if (parent?.type == CalloutBlockKeys.type) { + newPadding = padding.copyWith(right: padding.right + 10); + } + child = Padding(padding: newPadding, child: child); + + return child; + } + + @override + Node get currentNode => node; + + @override + EdgeInsets get boxPadding => padding; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart new file mode 100644 index 0000000000..c894811522 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart @@ -0,0 +1,77 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/widgets.dart'; + +abstract class DefaultSelectableMixinState + extends State with SelectableMixin { + final widgetKey = GlobalKey(); + RenderBox? get _renderBox => + widgetKey.currentContext?.findRenderObject() as RenderBox?; + + Node get currentNode; + + EdgeInsets get boxPadding => EdgeInsets.zero; + + @override + Position start() => Position(path: currentNode.path); + + @override + Position end() => Position(path: currentNode.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 box = _renderBox; + if (box is RenderBox) { + return boxPadding.topLeft & box.size; + } + return Rect.zero; + } + + @override + Rect? getCursorRectInPosition( + Position position, { + bool shiftWithBaseOffset = false, + }) { + final rects = getRectsInSelection(Selection.collapsed(position)); + return rects.firstOrNull; + } + + @override + List getRectsInSelection( + Selection selection, { + bool shiftWithBaseOffset = false, + }) { + if (_renderBox == null) { + return []; + } + final parentBox = context.findRenderObject(); + final box = widgetKey.currentContext?.findRenderObject(); + if (parentBox is RenderBox && box is RenderBox) { + return [ + box.localToGlobal(Offset.zero, ancestor: parentBox) & box.size, + ]; + } + return [Offset.zero & _renderBox!.size]; + } + + @override + Selection getSelectionInRange(Offset start, Offset end) => Selection.single( + path: currentNode.path, + startOffset: 0, + endOffset: 1, + ); + + @override + Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => + _renderBox!.localToGlobal(offset); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart new file mode 100644 index 0000000000..f05f4a9706 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart @@ -0,0 +1,88 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy_backend/log.dart'; + +import 'package:http/http.dart' as http; +// ignore: depend_on_referenced_packages +import 'package:html/parser.dart' as html_parser; + +abstract class LinkInfoParser { + Future parse( + Uri link, { + Duration timeout = const Duration(seconds: 8), + Map? headers, + }); + + static String formatUrl(String url) { + Uri? uri = Uri.tryParse(url); + if (uri == null) return url; + if (!uri.hasScheme) uri = Uri.tryParse('http://$url'); + if (uri == null) return url; + final isHome = (uri.hasEmptyPath || uri.path == '/') && !uri.hasQuery; + final homeUrl = '${uri.scheme}://${uri.host}/'; + if (isHome) return homeUrl; + return '$uri'; + } +} + +class DefaultParser implements LinkInfoParser { + @override + Future parse( + Uri link, { + Duration timeout = const Duration(seconds: 8), + Map? headers, + }) async { + try { + final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; + final http.Response response = + await http.get(link, headers: headers).timeout(timeout); + final code = response.statusCode; + if (code != 200 && isHome) { + throw Exception('Http request error: $code'); + } + // else if (!isHome && code == 403) { + // uri = Uri.parse('${uri.scheme}://${uri.host}/'); + // response = await http.get(uri).timeout(timeout); + // } + + final document = html_parser.parse(response.body); + + final siteName = document + .querySelector('meta[property="og:site_name"]') + ?.attributes['content']; + + String? title = document + .querySelector('meta[property="og:title"]') + ?.attributes['content']; + title ??= document.querySelector('title')?.text; + + String? description = document + .querySelector('meta[property="og:description"]') + ?.attributes['content']; + description ??= document + .querySelector('meta[name="description"]') + ?.attributes['content']; + + String? imageUrl = document + .querySelector('meta[property="og:image"]') + ?.attributes['content']; + if (imageUrl != null && !imageUrl.startsWith('http')) { + imageUrl = link.resolve(imageUrl).toString(); + } + + final favicon = + 'https://www.google.com/s2/favicons?sz=64&domain=${link.host}'; + + return LinkInfo( + url: '$link', + siteName: siteName, + title: title, + description: description, + imageUrl: imageUrl, + faviconUrl: favicon, + ); + } catch (e) { + Log.error('Parse link $link error: $e'); + return null; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart new file mode 100644 index 0000000000..6f1ac6fb22 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:http/http.dart' as http; +import 'default_parser.dart'; + +class YoutubeParser implements LinkInfoParser { + @override + Future parse( + Uri link, { + Duration timeout = const Duration(seconds: 8), + Map? headers, + }) async { + try { + final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; + if (isHome) { + return DefaultParser().parse( + link, + timeout: timeout, + headers: headers, + ); + } + + final requestLink = + 'https://www.youtube.com/oembed?url=$link&format=json'; + final http.Response response = await http + .get(Uri.parse(requestLink), headers: headers) + .timeout(timeout); + final code = response.statusCode; + if (code != 200) { + throw Exception('Http request error: $code'); + } + + final youtubeInfo = YoutubeInfo.fromJson(jsonDecode(response.body)); + + final favicon = + 'https://www.google.com/s2/favicons?sz=64&domain=${link.host}'; + return LinkInfo( + url: '$link', + title: youtubeInfo.title, + siteName: youtubeInfo.authorName, + imageUrl: youtubeInfo.thumbnailUrl, + faviconUrl: favicon, + ); + } catch (e) { + Log.error('Parse link $link error: $e'); + return null; + } + } +} + +class YoutubeInfo { + YoutubeInfo({ + this.title, + this.authorName, + this.version, + this.providerName, + this.providerUrl, + this.thumbnailUrl, + }); + + YoutubeInfo.fromJson(Map json) { + title = json['title']; + authorName = json['author_name']; + version = json['version']; + providerName = json['provider_name']; + providerUrl = json['provider_url']; + thumbnailUrl = json['thumbnail_url']; + } + String? title; + String? authorName; + String? version; + String? providerName; + String? providerUrl; + String? thumbnailUrl; + + Map toJson() => { + 'title': title, + 'author_name': authorName, + 'version': version, + 'provider_name': providerName, + 'provider_url': providerUrl, + 'thumbnail_url': thumbnailUrl, + }; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart deleted file mode 100644 index 6688cfe304..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; - -class LinkPreviewDataCache implements LinkPreviewDataCacheInterface { - @override - Future get(String url) async { - final option = - await getIt().getWithFormat( - url, - (value) => LinkPreviewData.fromJson(jsonDecode(value)), - ); - return option; - } - - @override - Future set(String url, LinkPreviewData data) async { - await getIt().set( - url, - jsonEncode(data.toJson()), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart index 16febe54cb..d31b2f8fd9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart @@ -3,8 +3,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -28,11 +28,10 @@ class PasteAsMenuService { void dismiss() { if (_menuEntry != null) { - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); keepEditorFocusNotifier.decrease(); + // editorState.service.scrollService?.enable(); + // editorState.service.keyboardService?.enable(); } - _menuEntry?.remove(); _menuEntry = null; } @@ -58,6 +57,7 @@ class PasteAsMenuService { children: [ ltrb.buildPositioned( child: PasteAsMenu( + editorState: editorState, onSelect: (t) { final selection = editorState.selection; if (selection == null) return; @@ -91,8 +91,9 @@ class PasteAsMenuService { Overlay.of(context).insert(_menuEntry!); - editorState.service.keyboardService?.disable(showCursor: true); - editorState.service.scrollService?.disable(); + keepEditorFocusNotifier.increase(); + // editorState.service.keyboardService?.disable(showCursor: true); + // editorState.service.scrollService?.disable(); } } @@ -101,9 +102,11 @@ class PasteAsMenu extends StatefulWidget { super.key, required this.onSelect, required this.onDismiss, + required this.editorState, }); final ValueChanged onSelect; final VoidCallback onDismiss; + final EditorState editorState; @override State createState() => _PasteAsMenuState(); @@ -113,24 +116,28 @@ class _PasteAsMenuState extends State { final focusNode = FocusNode(debugLabel: 'paste_as_menu'); final ValueNotifier selectedIndexNotifier = ValueNotifier(0); + EditorState get editorState => widget.editorState; + @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback( (_) => focusNode.requestFocus(), ); + editorState.selectionNotifier.addListener(dismiss); } @override void dispose() { focusNode.dispose(); selectedIndexNotifier.dispose(); + editorState.selectionNotifier.removeListener(dismiss); super.dispose(); } @override Widget build(BuildContext context) { - final themeV2 = AFThemeExtensionV2.of(context); + final theme = AppFlowyTheme.of(context); return Focus( focusNode: focusNode, onKeyEvent: onKeyEvent, @@ -140,14 +147,8 @@ class _PasteAsMenuState extends State { padding: EdgeInsets.all(6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 16, - color: themeV2.shadow_medium, - ), - ], + color: theme.surfaceColorScheme.primary, + boxShadow: [theme.shadow.medium], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -156,7 +157,7 @@ class _PasteAsMenuState extends State { height: 32, padding: EdgeInsets.all(8), child: FlowyText.semibold( - color: themeV2.text_tertiary, + color: theme.textColorScheme.primary, LocaleKeys.document_plugins_linkPreview_typeSelection_pasteAs .tr(), ), @@ -201,8 +202,11 @@ class _PasteAsMenuState extends State { length = PasteMenuType.values.length; if (event.logicalKey == LogicalKeyboardKey.enter) { onSelect(PasteMenuType.values[index]); + return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.escape) { dismiss(); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + dismiss(); } else if ([LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowLeft] .contains(event.logicalKey)) { if (index == 0) { @@ -211,6 +215,7 @@ class _PasteAsMenuState extends State { index--; } changeIndex(index); + return KeyEventResult.handled; } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowRight] .contains(event.logicalKey)) { if (index == length - 1) { @@ -219,8 +224,9 @@ class _PasteAsMenuState extends State { index++; } changeIndex(index); + return KeyEventResult.handled; } - return KeyEventResult.handled; + return KeyEventResult.ignored; } void onSelect(PasteMenuType type) => widget.onSelect.call(type); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart index 7a807fef64..06ebcb5002 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart @@ -42,8 +42,8 @@ class MentionLinkBlock extends StatefulWidget { class _MentionLinkBlockState extends State { final parser = LinkParser(); _LoadingStatus status = _LoadingStatus.loading; + late LinkInfo linkInfo = LinkInfo(url: url); final previewController = PopoverController(); - LinkInfo linkInfo = LinkInfo(); bool isHovering = false; int previewFocusNum = 0; bool isPreviewHovering = false; @@ -67,13 +67,14 @@ class _MentionLinkBlockState extends State { super.initState(); parser.addLinkInfoListener((v) { + final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); if (mounted) { setState(() { - if (v.isEmpty() && linkInfo.isEmpty()) { - status = _LoadingStatus.error; - } else { + if (hasNewInfo) { linkInfo = v; status = _LoadingStatus.idle; + } else if (!hasOldInfo) { + status = _LoadingStatus.error; } }); } @@ -148,6 +149,7 @@ class _MentionLinkBlockState extends State { Widget buildIconWithTitle(BuildContext context) { final theme = AppFlowyTheme.of(context); + final siteName = linkInfo.siteName, linkTitle = linkInfo.title ?? url; return MouseRegion( cursor: SystemMouseCursors.click, @@ -169,12 +171,25 @@ class _MentionLinkBlockState extends State { buildIcon(), HSpace(4), Flexible( - child: FlowyText( - linkInfo.siteName ?? url, - color: theme.textColorScheme.primary, - fontSize: 14, - figmaLineHeight: 20, + child: RichText( overflow: TextOverflow.ellipsis, + text: TextSpan( + children: [ + if (siteName != null) ...[ + TextSpan( + text: siteName, + style: theme.textStyle.body + .standard(color: theme.textColorScheme.secondary), + ), + WidgetSpan(child: HSpace(2)), + ], + TextSpan( + text: linkTitle, + style: theme.textStyle.body + .standard(color: theme.textColorScheme.primary), + ), + ], + ), ), ), HSpace(2), @@ -323,9 +338,10 @@ class _MentionLinkBlockState extends State { maxHeight: 48 + size.height, ); } + final hasImage = linkInfo.imageUrl?.isNotEmpty ?? false; return BoxConstraints( maxWidth: max(300, size.width), - maxHeight: 300, + maxHeight: hasImage ? 300 : 180, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart index 00082f127a..00b161379e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart @@ -56,7 +56,9 @@ class _MentionLinkPreviewState extends State { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context), textColorScheme = theme.textColorScheme; - + final imageUrl = linkInfo.imageUrl ?? '', + description = linkInfo.description ?? ''; + final imageHeight = 120.0; final card = MouseRegion( onEnter: widget.onEnter, onExit: widget.onExit, @@ -67,20 +69,21 @@ class _MentionLinkPreviewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16)), - child: FlowyNetworkImage( - url: linkInfo.imageUrl ?? '', - width: 280, - height: 120, + if (imageUrl.isNotEmpty) + ClipRRect( + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + child: FlowyNetworkImage( + url: linkInfo.imageUrl ?? '', + width: 280, + height: imageHeight, + ), ), - ), VSpace(12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: FlowyText.semibold( - linkInfo.siteName ?? '', + linkInfo.title ?? linkInfo.siteName ?? '', fontSize: 14, figmaLineHeight: 20, color: textColorScheme.primary, @@ -88,18 +91,20 @@ class _MentionLinkPreviewState extends State { ), ), VSpace(4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyText( - linkInfo.description ?? '', - fontSize: 12, - figmaLineHeight: 16, - color: textColorScheme.secondary, - maxLines: 3, - overflow: TextOverflow.ellipsis, + if (description.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyText( + description, + fontSize: 12, + figmaLineHeight: 16, + color: textColorScheme.secondary, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), ), - ), - VSpace(36), + VSpace(36), + ], Container( margin: const EdgeInsets.symmetric(horizontal: 16), height: 28, @@ -109,7 +114,7 @@ class _MentionLinkPreviewState extends State { HSpace(6), Expanded( child: FlowyText( - linkInfo.description ?? '', + linkInfo.siteName ?? linkInfo.url, fontSize: 12, figmaLineHeight: 16, color: textColorScheme.primary, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 63aaf34e8d..4161036a08 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -40,7 +40,6 @@ export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'keyboard_interceptor/keyboard_interceptor.dart'; export 'link_preview/custom_link_preview.dart'; -export 'link_preview/link_preview_cache.dart'; export 'link_preview/link_preview_menu.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index 77d94451c8..7dbf192bae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -6,8 +6,8 @@ import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; @@ -70,6 +70,7 @@ class _FormatToolbarItem extends ToolbarItem { ? highlightColor : EditorStyleCustomizer.toolbarHoverColor(context); final isDark = !Theme.of(context).isLightMode; + final theme = AppFlowyTheme.of(context); final child = FlowyIconButton( width: 36, @@ -81,7 +82,7 @@ class _FormatToolbarItem extends ToolbarItem { size: Size.square(20.0), color: (isDark && isHighlight) ? Color(0xFF282E3A) - : AFThemeExtensionV2.of(context).icon_primary, + : theme.iconColorTheme.primary, ), onPressed: () => editorState.toggleAttribute( name, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index cd332e14ef..2e115d240d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -3,7 +3,7 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; -import 'package:flowy_infra/theme_extension_v2.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -82,7 +82,8 @@ class _HighlightColorPickerWidgetState } Widget buildChild(BuildContext context) { - final iconColor = AFThemeExtensionV2.of(context).icon_primary; + final theme = AppFlowyTheme.of(context), + iconColor = theme.iconColorTheme.primary; final child = FlowyIconButton( width: 36, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index 693c7a64ce..cbbce9c943 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -8,7 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -34,7 +34,7 @@ final customLinkItem = ToolbarItem( final hoverColor = isHref ? highlightColor : EditorStyleCustomizer.toolbarHoverColor(context); - + final theme = AppFlowyTheme.of(context); final child = FlowyIconButton( width: 36, height: 32, @@ -45,7 +45,7 @@ final customLinkItem = ToolbarItem( size: Size.square(20.0), color: (isDark && isHref) ? Color(0xFF282E3A) - : AFThemeExtensionV2.of(context).icon_primary, + : theme.iconColorTheme.primary, ), onPressed: () { getIt().hideToolbar(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index d751728526..2a1688db19 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -3,8 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -96,7 +96,8 @@ class _TextAlignActionListState extends State { } Widget buildChild(BuildContext context) { - final themeV2 = AFThemeExtensionV2.of(context); + final theme = AppFlowyTheme.of(context), + iconColor = theme.iconColorTheme.primary; final child = FlowyIconButton( width: 48, height: 32, @@ -108,13 +109,13 @@ class _TextAlignActionListState extends State { FlowySvg( FlowySvgs.toolbar_alignment_m, size: Size.square(20), - color: themeV2.icon_primary, + color: iconColor, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: themeV2.icon_tertiary, + color: iconColor, ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 525eebe917..80f2d3138d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -4,8 +4,8 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'toolbar_id_enum.dart'; @@ -57,6 +57,9 @@ class _TextColorPickerWidgetState extends State { @override Widget build(BuildContext context) { + if (editorState.selection == null) { + return const SizedBox.shrink(); + } final selectionRectList = editorState.selectionRects(); final top = selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; @@ -78,7 +81,8 @@ class _TextColorPickerWidgetState extends State { } Widget buildChild(BuildContext context) { - final iconColor = AFThemeExtensionV2.of(context).icon_primary; + final theme = AppFlowyTheme.of(context), + iconColor = theme.iconColorTheme.primary; final child = FlowyIconButton( width: 36, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 625fedff79..8140a7b7f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -4,8 +4,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -78,7 +78,8 @@ class _TextHeadingActionListState extends State { } Widget buildChild(BuildContext context) { - final themeV2 = AFThemeExtensionV2.of(context); + final theme = AppFlowyTheme.of(context), + iconColor = theme.iconColorTheme.primary; final child = FlowyIconButton( width: 48, height: 32, @@ -90,13 +91,13 @@ class _TextHeadingActionListState extends State { FlowySvg( FlowySvgs.toolbar_text_format_m, size: Size.square(20), - color: themeV2.icon_primary, + color: iconColor, ), HSpace(4), FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: themeV2.icon_tertiary, + color: iconColor, ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index a0aded3f8e..3c8f55caef 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -8,9 +8,9 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -119,8 +119,8 @@ class _SuggestionsActionListState extends State { } Widget buildChild(BuildContext context) { - final themeV2 = AFThemeExtensionV2.of(context); - + final theme = AppFlowyTheme.of(context), + iconColor = theme.iconColorTheme.primary; final child = FlowyHover( isSelected: () => isSelected, style: HoverStyle( @@ -163,7 +163,7 @@ class _SuggestionsActionListState extends State { FlowySvg( FlowySvgs.toolbar_arrow_down_m, size: Size(12, 20), - color: themeV2.icon_tertiary, + color: iconColor, ), ], ), diff --git a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart index 5c5e788ddb..5942271206 100644 --- a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart +++ b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart @@ -100,7 +100,6 @@ class _DesktopSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index d8dc4dd121..184cc9dd09 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -136,7 +136,6 @@ class AppFlowyCloudDeepLink { final context = AppGlobals.rootNavKey.currentState?.context; if (context != null) { showToastNotification( - message: err.msg, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index 51c693843f..ccad6c0a26 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -17,14 +17,12 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { case ErrorCode.InvalidEncryptSecret: case ErrorCode.NetworkError: showToastNotification( - message: error.msg, type: ToastificationType.error, ); break; default: showToastNotification( - message: error.msg, type: ToastificationType.error, callbacks: ToastificationCallbacks( diff --git a/frontend/appflowy_flutter/lib/util/share_log_files.dart b/frontend/appflowy_flutter/lib/util/share_log_files.dart index 0435c5fe52..b8dd390627 100644 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ b/frontend/appflowy_flutter/lib/util/share_log_files.dart @@ -25,7 +25,6 @@ Future shareLogFiles(BuildContext? context) async { if (archiveLogFiles.isEmpty) { if (context != null && context.mounted) { showToastNotification( - message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -42,7 +41,6 @@ Future shareLogFiles(BuildContext? context) async { if (zip == null) { if (context != null && context.mounted) { showToastNotification( - message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -72,7 +70,6 @@ Future shareLogFiles(BuildContext? context) async { } catch (e) { if (context != null && context.mounted) { showToastNotification( - message: e.toString(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 568fd76db0..2a707b6b2d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -2,7 +2,6 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @@ -152,11 +151,6 @@ class DesktopAppearance extends BaseAppearance { lightIconColor: theme.lightIconColor, toolbarHoverColor: theme.toolbarHoverColor, ), - isLight - ? lightAFThemeV2 - : darkAFThemeV2.copyWith( - icon_primary: theme.icon, - ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 7778f1c9ce..eda3153459 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -4,7 +4,6 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/theme_extension_v2.dart'; import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { @@ -30,7 +29,6 @@ class MobileAppearance extends BaseAppearance { ); final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - final isLight = brightness == Brightness.light; final theme = brightness == Brightness.light ? appTheme.lightTheme @@ -285,11 +283,6 @@ class MobileAppearance extends BaseAppearance { toolbarHoverColor: theme.toolbarHoverColor, ), ToolbarColorExtension.fromBrightness(brightness), - isLight - ? lightAFThemeV2 - : darkAFThemeV2.copyWith( - icon_primary: theme.icon, - ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 20160d32ec..50ea9d83c7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -169,7 +169,6 @@ class _SidebarWorkspaceState extends State { if (message != null) { showToastNotification( - message: message, type: result.fold( (_) => ToastificationType.success, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 8cabffc6e3..e6c011156b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -193,7 +193,6 @@ Future deleteMyAccount( if (!isChecked) { showToastNotification( - type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -208,7 +207,6 @@ Future deleteMyAccount( if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) { showToastNotification( - type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -226,7 +224,6 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - message: LocaleKeys .newSettings_myAccount_deleteAccount_deleteAccountSuccess .tr(), @@ -245,7 +242,6 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - type: ToastificationType.error, bottomPadding: bottomPadding, message: f.msg, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 194ad02beb..a2d911ea40 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -157,7 +157,6 @@ class SettingsManageDataView extends StatelessWidget { if (context.mounted) { showToastNotification( - message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index c17dd14578..32b3335d0f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -8,6 +8,7 @@ import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; import 'package:appflowy_ui/src/theme/dimensions.dart'; +import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; import 'package:flutter/material.dart'; @@ -92,6 +93,35 @@ class AppFlowyThemeBuilder { }; } + AppFlowyShadow buildShadow(Brightness brightness) { + return switch (brightness) { + Brightness.light => AppFlowyShadow( + small: const BoxShadow( + offset: Offset(0.0, 2.0), + blurRadius: 16.0, + color: Color(0x1F000000), + ), + medium: const BoxShadow( + offset: Offset(0.0, 4.0), + blurRadius: 32.0, + color: Color(0x1F000000), + ), + ), + Brightness.dark => AppFlowyShadow( + small: BoxShadow( + offset: Offset(0.0, 2.0), + blurRadius: 16.0, + color: Color(0x7A000000), + ), + medium: BoxShadow( + offset: Offset(0.0, 4.0), + blurRadius: 32.0, + color: Color(0x7A000000), + ), + ), + }; + } + AppFlowyBorderColorScheme buildBorderColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart index 68fb378c0f..9494bdf0e2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart @@ -8,6 +8,7 @@ import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; import 'package:appflowy_ui/src/theme/data/builder.dart'; +import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; import 'package:appflowy_ui/src/theme/text_style/text_style.dart'; import 'package:flutter/material.dart'; @@ -36,6 +37,8 @@ abstract class AppFlowyBaseTheme { AppFlowySpacing get spacing; AppFlowyBrandColorScheme get brandColorScheme; + + AppFlowyShadow get shadow; } class AppFlowyThemeData extends AppFlowyBaseTheme { @@ -67,10 +70,10 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.light, ); + final shadow = themeBuilder.buildShadow(Brightness.light); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); - return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -83,6 +86,7 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { borderRadius: borderRadius, spacing: spacing, brandColorScheme: brandColorScheme, + shadow: shadow, ); } @@ -113,10 +117,10 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.dark, ); + final shadow = themeBuilder.buildShadow(Brightness.dark); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); - return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -129,6 +133,7 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { borderRadius: borderRadius, spacing: spacing, brandColorScheme: brandColorScheme, + shadow: shadow, ); } @@ -144,6 +149,7 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { required this.brandColorScheme, required this.iconColorTheme, required this.backgroundColorScheme, + required this.shadow, this.brightness = Brightness.light, }); @@ -184,6 +190,9 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { @override final AppFlowyBackgroundColorScheme backgroundColorScheme; + @override + final AppFlowyShadow shadow; + static AppFlowyTextColorScheme buildTextColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart new file mode 100644 index 0000000000..9bb2ac1116 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +class AppFlowyShadow { + AppFlowyShadow({required this.small, required this.medium}); + + final BoxShadow small; + final BoxShadow medium; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 5ad2435e99..6f37058f00 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -81,7 +81,7 @@ String languageFromLocale(Locale locale) { case "ur": return "اردو"; case "hin": - return "हिन्दी"; + return "हिन्दी"; } // If not found then the language code will be displayed return locale.languageCode; diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart deleted file mode 100644 index b9136a00bc..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension_v2.dart +++ /dev/null @@ -1,99 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'package:flutter/material.dart'; - -@immutable -class AFThemeExtensionV2 extends ThemeExtension { - static AFThemeExtensionV2 of(BuildContext context) => - Theme.of(context).extension()!; - - static AFThemeExtensionV2? maybeOf(BuildContext context) => - Theme.of(context).extension(); - - const AFThemeExtensionV2({ - required this.icon_primary, - required this.icon_tertiary, - required this.text_tertiary, - required this.border_grey_quaternary, - required this.fill_theme_select, - required this.fill_grey_thick_alpha_1, - required this.shadow_medium, - }); - - final Color icon_primary; - final Color icon_tertiary; - final Color text_tertiary; - final Color border_grey_quaternary; - final Color fill_theme_select; - final Color fill_grey_thick_alpha_1; - final Color shadow_medium; - - @override - AFThemeExtensionV2 copyWith({ - Color? icon_primary, - Color? icon_tertiary, - Color? text_tertiary, - Color? border_grey_quaternary, - Color? fill_theme_select, - Color? fill_grey_thick_alpha_1, - Color? shadow_medium, - }) => - AFThemeExtensionV2( - icon_primary: icon_primary ?? this.icon_primary, - icon_tertiary: icon_tertiary ?? this.icon_tertiary, - text_tertiary: text_tertiary ?? this.text_tertiary, - border_grey_quaternary: - border_grey_quaternary ?? this.border_grey_quaternary, - fill_theme_select: fill_theme_select ?? this.fill_theme_select, - fill_grey_thick_alpha_1: - fill_grey_thick_alpha_1 ?? this.fill_grey_thick_alpha_1, - shadow_medium: shadow_medium ?? this.shadow_medium, - ); - - @override - ThemeExtension lerp( - ThemeExtension? other, double t) { - if (other is! AFThemeExtensionV2) { - return this; - } - return AFThemeExtensionV2( - icon_primary: - Color.lerp(icon_primary, other.icon_primary, t) ?? icon_primary, - icon_tertiary: - Color.lerp(icon_tertiary, other.icon_tertiary, t) ?? icon_tertiary, - text_tertiary: - Color.lerp(text_tertiary, other.text_tertiary, t) ?? text_tertiary, - border_grey_quaternary: - Color.lerp(border_grey_quaternary, other.border_grey_quaternary, t) ?? - border_grey_quaternary, - fill_theme_select: - Color.lerp(fill_theme_select, other.fill_theme_select, t) ?? - fill_theme_select, - fill_grey_thick_alpha_1: Color.lerp( - fill_grey_thick_alpha_1, other.fill_grey_thick_alpha_1, t) ?? - fill_grey_thick_alpha_1, - shadow_medium: - Color.lerp(shadow_medium, other.shadow_medium, t) ?? shadow_medium, - ); - } -} - -const AFThemeExtensionV2 darkAFThemeV2 = AFThemeExtensionV2( - icon_primary: Color(0xFF1F2329), - icon_tertiary: Color(0xFF99A1A8), - text_tertiary: Color(0xFFB5BBD3), - border_grey_quaternary: Color(0xFFE8ECF3), - fill_theme_select: Color(0x00BCF01F), - fill_grey_thick_alpha_1: Color(0x1F23290F), - shadow_medium: Color(0x1F22251F), -); - -const AFThemeExtensionV2 lightAFThemeV2 = AFThemeExtensionV2( - icon_primary: Color(0xFF1F2329), - icon_tertiary: Color(0xFF99A1A8), - text_tertiary: Color(0xFFB5BBD3), - border_grey_quaternary: Color(0xFFE8ECF3), - fill_theme_select: Color(0x00BCF01F), - fill_grey_thick_alpha_1: Color(0x1F23290F), - shadow_medium: Color(0x1F22251F), -); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index ae44b95eb3..1c6d66431d 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -673,14 +673,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" - favicon: - dependency: "direct main" - description: - name: favicon - sha256: ebb7423ba7ccc87d3cce9b60de1b5f72004de3cddeedd88d98463fc7f66faf0c - url: "https://pub.dev" - source: hosted - version: "1.1.2" ffi: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index de26c31c1e..0cc553d1a7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: auto_size_text_field: ^2.2.3 auto_updater: ^1.0.0 avatar_stack: ^3.0.0 - favicon: ^1.1.2 # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 diff --git a/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart b/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart new file mode 100644 index 0000000000..5b6f88801a --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart @@ -0,0 +1,47 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + test( + 'description', + () async { + final links = [ + 'https://www.baidu.com/', + 'https://appflowy.io/', + 'https://github.com/AppFlowy-IO/AppFlowy', + 'https://github.com/', + 'https://www.figma.com/design/3K0ai4FhDOJ3Lts8G3KOVP/Page?node-id=7282-4007&p=f&t=rpfvEvh9K9J9WkIo-0', + 'https://www.figma.com/files/drafts', + 'https://www.youtube.com/watch?v=LyY5Rh9qBvA', + 'https://www.youtube.com/', + 'https://www.youtube.com/watch?v=a6GDT7', + 'http://www.test.com/', + 'https://www.baidu.com/s?wd=test&rsv_spt=1&rsv_iqid=0xb6a7840b00e5324a&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=22073068_7_oem_dg&rsv_dl=tb&rsv_enter=1&rsv_sug3=5&rsv_sug1=4&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=test&rsp=9&inputT=478&rsv_sug4=547', + 'https://www.google.com/', + 'https://www.google.com.hk/search?q=test&oq=test&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIHCAQQABiABDIHCAUQABiABDIHCAYQABiABDIHCAcQABiABDIHCAgQLhiABDIHCAkQABiABNIBCTE4MDJqMGoxNagCCLACAfEFAQs7K9PprSfxBQELOyvT6a0n&sourceid=chrome&ie=UTF-8', + 'www.baidu.com', + 'baidu.com', + 'com', + 'https://www.baidu.com', + 'https://github.com/AppFlowy-IO/AppFlowy', + 'https://appflowy.com/app/c29fafc4-b7c0-4549-8702-71339b0fd9ea/59f36be8-9b2f-4d3e-b6a1-816c6c2043e5?blockId=GCY_T4', + ]; + + final parser = DefaultParser(); + int i = 1; + for (final link in links) { + final formatLink = LinkInfoParser.formatUrl(link); + final siteInfo = await parser + .parse(Uri.tryParse(formatLink) ?? Uri.parse(formatLink)); + if (siteInfo?.isEmpty() ?? true) { + debugPrint('$i : $formatLink ---- empty \n'); + } else { + debugPrint('$i : $formatLink ---- \n$siteInfo \n'); + } + i++; + } + }, + timeout: const Timeout(Duration(seconds: 120)), + ); +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 91f9ce7743..46cab3030a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2041,7 +2041,7 @@ "reload": "Reload", "removeLink": "Remove Link", "pasteHint": "Paste in https://...", - "refuseConnect": "refued to connect." + "unableToDisplay": "unable to display" } } }, diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 2731a0fb2e..d1433929bc 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1517,6 +1517,27 @@ "placeholder": "粘贴视频链接", "copiedToPasteBoard": "视频链接已复制到剪贴板", "insertVideo": "添加视频" + }, + "linkPreview": { + "typeSelection": { + "pasteAs": "粘贴为", + "mention": "提及", + "URL": "URL", + "bookmark": "书签", + "embed": "嵌入" + }, + "linkPreviewMenu": { + "toMetion": "转换为提及", + "toUrl": "转换为URL", + "toEmbed": "转换为嵌入", + "toBookmark": "转换为书签", + "copyLink": "复制链接", + "replace": "替换", + "reload": "重新加载", + "removeLink": "移除链接", + "pasteHint": "粘贴 https://...", + "unableToDisplay": "无法显示" + } } }, "outlineBlock": { From 437deaf986353c72d8281a3bc4ae05746e61f834 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 14 Apr 2025 14:38:25 +0800 Subject: [PATCH 307/384] chore: update logs --- .../command_palette/folder_search_test.dart | 12 +- .../widgets/search_result_tile.dart | 67 ++++---- .../widgets/search_results_list.dart | 161 +++++++++++++++--- frontend/appflowy_flutter/macos/Podfile.lock | 46 ++--- frontend/rust-lib/Cargo.lock | 24 +-- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-search/src/document/handler.rs | 12 +- .../flowy-search/src/entities/result.rs | 4 +- .../flowy-search/src/services/manager.rs | 17 +- 9 files changed, 233 insertions(+), 114 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart index 265d313320..a1d7f12667 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart @@ -44,11 +44,11 @@ void main() { await tester.pumpAndSettle(const Duration(milliseconds: 200)); // Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna) - expect(find.byType(SearchResultTile), findsNWidgets(2)); + expect(find.byType(SearchResultCell), findsNWidgets(2)); // The score should be higher for "ViewOna" thus it should be shown first final secondDocumentWidget = tester - .widget(find.byType(SearchResultTile).first) as SearchResultTile; + .widget(find.byType(SearchResultCell).first) as SearchResultCell; expect(secondDocumentWidget.item.data, secondDocument); // Change search to "ViewOne" @@ -57,8 +57,8 @@ void main() { // The score should be higher for "ViewOne" thus it should be shown first final firstDocumentWidget = tester.widget( - find.byType(SearchResultTile).first, - ) as SearchResultTile; + find.byType(SearchResultCell).first, + ) as SearchResultCell; expect(firstDocumentWidget.item.data, firstDocument); }); @@ -89,11 +89,11 @@ void main() { ); await tester.enterText(searchFieldFinder, 'Page-$randomValue'); await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(find.byType(SearchResultTile), findsNWidgets(2)); + expect(find.byType(SearchResultCell), findsNWidgets(2)); /// check results final svgs = find.descendant( - of: find.byType(SearchResultTile), + of: find.byType(SearchResultCell), matching: find.byType(FlowySvg), ); expect(svgs, findsNWidgets(2)); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart index a11721d1c3..551823f08e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -13,23 +13,25 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -class SearchResultTile extends StatefulWidget { - const SearchResultTile({ +class SearchResultCell extends StatefulWidget { + const SearchResultCell({ super.key, required this.item, required this.onSelected, + required this.onHover, this.isTrashed = false, }); final SearchResponseItemPB item; final VoidCallback onSelected; + final Function(SearchResponseItemPB) onHover; final bool isTrashed; @override - State createState() => _SearchResultTileState(); + State createState() => _SearchResultCellState(); } -class _SearchResultTileState extends State { +class _SearchResultCellState extends State { bool _hasFocus = false; final focusNode = FocusNode(); @@ -132,33 +134,36 @@ class _SearchResultTileState extends State { ); } - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleSelection, - child: Focus( - focusNode: focusNode, - onKeyEvent: (node, event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.enter) { - _handleSelection(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), - child: FlowyHover( - isSelected: () => _hasFocus, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: tileContent, + return MouseRegion( + onEnter: (_) => widget.onHover(widget.item), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleSelection, + child: Focus( + focusNode: focusNode, + onKeyEvent: (node, event) { + if (event is! KeyDownEvent) return KeyEventResult.ignored; + if (event.logicalKey == LogicalKeyboardKey.enter) { + _handleSelection(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), + child: FlowyHover( + isSelected: () => _hasFocus, + style: HoverStyle( + borderRadius: BorderRadius.circular(8), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: tileContent, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index b2ca24b68f..2eb3b44b3e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -7,7 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -class SearchResultList extends StatelessWidget { +class SearchResultList extends StatefulWidget { const SearchResultList({ required this.trash, required this.resultItems, @@ -19,6 +19,13 @@ class SearchResultList extends StatelessWidget { final List resultItems; final List resultSummaries; + @override + State createState() => _SearchResultListState(); +} + +class _SearchResultListState extends State { + dynamic _selectedCellData; + Widget _buildSectionHeader(String title) => Padding( padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8), @@ -28,6 +35,18 @@ class SearchResultList extends StatelessWidget { ), ); + void _onHoverSummary(SearchSummaryPB summary) { + setState(() { + _selectedCellData = summary; + }); + } + + void _onHoverResult(SearchResponseItemPB item) { + setState(() { + _selectedCellData = item; + }); + } + Widget _buildSummariesSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -36,10 +55,11 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultSummaries.length, + itemCount: widget.resultSummaries.length, separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) => SearchSummaryTile( - summary: resultSummaries[index], + itemBuilder: (_, index) => SearchSummaryCell( + summary: widget.resultSummaries[index], + onHover: _onHoverSummary, ), ), ], @@ -55,14 +75,15 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultItems.length, + itemCount: widget.resultItems.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) { - final item = resultItems[index]; - return SearchResultTile( + final item = widget.resultItems[index]; + return SearchResultCell( item: item, onSelected: () => FlowyOverlay.pop(context), - isTrashed: trash.any((t) => t.id == item.viewId), + isTrashed: widget.trash.any((t) => t.id == item.viewId), + onHover: _onHoverResult, ); }, ), @@ -74,31 +95,127 @@ class SearchResultList extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), - child: ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), + child: Row( children: [ - if (resultSummaries.isNotEmpty) _buildSummariesSection(), - const VSpace(10), - if (resultItems.isNotEmpty) _buildResultsSection(context), + Flexible( + flex: 7, + child: ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + if (widget.resultSummaries.isNotEmpty) _buildSummariesSection(), + const VSpace(10), + if (widget.resultItems.isNotEmpty) + _buildResultsSection(context), + ], + ), + ), + Flexible( + flex: 3, + child: SearchCellDetail(cellData: _selectedCellData), + ), ], ), ); } } -class SearchSummaryTile extends StatelessWidget { - const SearchSummaryTile({required this.summary, super.key}); +class SearchCellDetail extends StatelessWidget { + const SearchCellDetail({ + super.key, + required this.cellData, + }); - final SearchSummaryPB summary; + final dynamic cellData; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: FlowyText( - summary.content, - maxLines: 10, + if (cellData == null) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: FlowyText( + 'Hover over an item to see details', + fontSize: 12, + color: Colors.grey, + ), + ), + ); + } + + if (cellData is SearchSummaryPB) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const FlowyText( + 'AI Summary', + fontSize: 14, + fontWeight: FontWeight.bold, + ), + const VSpace(8), + FlowyText( + cellData.content, + fontSize: 12, + ), + ], + ), + ); + } + + if (cellData is SearchResponseItemPB) { + final item = cellData as SearchResponseItemPB; + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + item.data, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + const VSpace(8), + if (item.preview.isNotEmpty) + FlowyText( + item.preview, + fontSize: 12, + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + } +} + +class SearchSummaryCell extends StatefulWidget { + const SearchSummaryCell({ + required this.summary, + required this.onHover, + super.key, + }); + + final SearchSummaryPB summary; + final Function(SearchSummaryPB) onHover; + + @override + State createState() => _SearchSummaryCellState(); +} + +class _SearchSummaryCellState extends State { + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => widget.onHover(widget.summary), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: FlowyText( + widget.summary.content, + maxLines: 3, + ), ), ); } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index fd3ae1a6b9..671d8bccf2 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "async-trait", @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "bincode", "bytes", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "app-error", "jsonwebtoken", @@ -4098,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "bytes", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 0a3cf409bb..8d8e11f6a0 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,8 +105,8 @@ tantivy = { version = "0.24.0" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index 45bf5864c7..b808b38e74 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,6 +1,6 @@ use crate::entities::{ CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB, - SearchSourcePB, SearchSummaryPB, + SearchSummaryPB, }; use crate::{ entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, @@ -82,7 +82,6 @@ impl SearchHandler for DocumentSearchHandler { }, }; - trace!("[Search] search results: {:?}", result_items); let summary_input = result_items .iter() .map(|v| SearchResult { @@ -137,13 +136,8 @@ impl SearchHandler for DocumentSearchHandler { let summaries: Vec = summary_result .summaries .into_iter() - .filter_map(|v| { - v.metadata.as_object().and_then(|object| { - let id = object.get("id")?.as_str()?.to_string(); - let source = object.get("source")?.as_str()?.to_string(); - let metadata = SearchSourcePB {id, source }; - Some(SearchSummaryPB { content: v.content, metadata: Some(metadata) }) - }) + .map(|v| { + SearchSummaryPB { content: v.content, source_ids: v.sources.iter().map(|id| id.to_string()).collect() } }) .collect(); diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index d7d6db0820..48de24728d 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -28,8 +28,8 @@ pub struct SearchSummaryPB { #[pb(index = 1)] pub content: String, - #[pb(index = 2, one_of)] - pub metadata: Option, + #[pb(index = 2)] + pub source_ids: Vec, } #[derive(ProtoBuf, Default, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index 30c6783fb0..157781eceb 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -62,16 +62,14 @@ impl SearchManager { search_id: String, ) { // Cancel previous search by updating current_search - { - let mut current = self.current_search.lock().await; - *current = Some(search_id.clone()); - } + *self.current_search.lock().await = Some(search_id.clone()); let handlers = self.handlers.clone(); let sink = IsolateSink::new(Isolate::new(stream_port)); let mut join_handles = vec![]; let current_search = self.current_search.clone(); + tracing::info!("[Search] perform search: {}", query); for (_, handler) in handlers { let mut clone_sink = sink.clone(); let query = query.clone(); @@ -80,11 +78,16 @@ impl SearchManager { let current_search = current_search.clone(); let handle = tokio::spawn(async move { + if !is_current_search(¤t_search, &search_id).await { + trace!("[Search] cancel search: {}", query); + return; + } + let mut stream = handler.perform_search(query.clone(), filter).await; while let Some(result) = stream.next().await { if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] search changed, cancel search: {}", query); - break; + trace!("[Search] discard search stream: {}", query); + return; } if let Ok(result) = result { @@ -103,7 +106,7 @@ impl SearchManager { } if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] search changed, cancel search: {}", query); + trace!("[Search] discard search result: {}", query); return; } From ddbaf0d530f12eb48bbe7cc8ecbce668473d1b9e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 14 Apr 2025 14:58:00 +0800 Subject: [PATCH 308/384] feat: otp improvement on mobile (#7738) --- .../setting/user_session_setting_group.dart | 11 +++++-- .../lib/user/application/sign_in_bloc.dart | 7 +++++ .../desktop_sign_in_screen.dart | 9 ++++-- .../sign_in_screen/mobile_sign_in_screen.dart | 13 +++++--- .../sign_in_screen/sign_in_screen.dart | 16 +++------- .../widgets/anonymous_sign_in_button.dart | 18 ++++------- .../continue_with_email_and_password.dart | 31 ++++++++++++++++--- .../widgets/sign_in_anonymous_button.dart | 25 ++++++++++----- .../macos/Runner/Configs/AppInfo.xcconfig | 2 +- .../lib/src/theme/data/builder.dart | 2 +- frontend/rust-lib/Cargo.lock | 6 ++-- frontend/rust-lib/Cargo.toml | 6 ++-- 12 files changed, 93 insertions(+), 53 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index b3b7cb71c5..617de1db50 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -63,8 +63,15 @@ class UserSessionSettingGroup extends StatelessWidget { ); }, builder: (context, state) { - return const ThirdPartySignInButtons( - expanded: true, + return Column( + children: [ + const ContinueWithEmailAndPassword(), + const VSpace(12.0), + const ThirdPartySignInButtons( + expanded: true, + ), + const VSpace(16.0), + ], ); }, ), diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 82269d1f1f..06a03765c9 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -186,6 +186,13 @@ class SignInBloc extends Bloc { Emitter emit, { required String email, }) async { + if (state.isSubmitting) { + Log.error('Sign in with magic link is already in progress'); + return; + } + + Log.info('Sign in with magic link: $email'); + emit( state.copyWith( isSubmitting: true, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 65e8ee550b..40901e92e1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/settings/show_settings.dart'; import 'package:appflowy/shared/window_title_bar.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; @@ -25,6 +26,7 @@ class DesktopSignInScreen extends StatelessWidget { return BlocBuilder( builder: (context, state) { + final bottomPadding = UniversalPlatform.isDesktop ? 20.0 : 24.0; return Scaffold( appBar: _buildAppBar(), body: Center( @@ -40,7 +42,10 @@ class DesktopSignInScreen extends StatelessWidget { VSpace(theme.spacing.xxl), // continue with email and password - const ContinueWithEmailAndPassword(), + isLocalAuthEnabled + ? const SignInAnonymousButtonV3() + : const ContinueWithEmailAndPassword(), + VSpace(theme.spacing.xxl), // third-party sign in. @@ -65,7 +70,7 @@ class DesktopSignInScreen extends StatelessWidget { SignInAnonymousButtonV2(), ], ), - const VSpace(16), + VSpace(bottomPadding), ], ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 6326e1a811..7e2222abf2 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -40,7 +40,7 @@ class MobileSignInScreen extends StatelessWidget { ? const SignInAnonymousButtonV3() : const ContinueWithEmailAndPassword(), const VSpace(spacing), - if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), + if (isAuthEnabled) _buildThirdPartySignInButtons(context), const VSpace(spacing * 1.5), const SignInAgreement(), const VSpace(spacing), @@ -75,7 +75,8 @@ class MobileSignInScreen extends StatelessWidget { ); } - Widget _buildThirdPartySignInButtons(ColorScheme colorScheme) { + Widget _buildThirdPartySignInButtons(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( children: [ Row( @@ -84,10 +85,12 @@ class MobileSignInScreen extends StatelessWidget { const Expanded(child: Divider()), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: FlowyText( + child: Text( LocaleKeys.signIn_or.tr(), - fontSize: 12, - color: colorScheme.onSecondary, + style: TextStyle( + fontSize: 16, + color: theme.textColorScheme.secondary, + ), ), ), const Expanded(child: Divider()), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index a428545d8d..afae06d50a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -2,15 +2,13 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; -import '../../helpers/helpers.dart'; - class SignInScreen extends StatelessWidget { const SignInScreen({super.key}); @@ -23,13 +21,9 @@ class SignInScreen extends StatelessWidget { child: BlocConsumer( listener: _showSignInError, builder: (context, state) { - final isLoading = context.read().state.isSubmitting; - if (UniversalPlatform.isMobile) { - return isLoading - ? const MobileLoadingScreen() - : const MobileSignInScreen(); - } - return const DesktopSignInScreen(); + return UniversalPlatform.isDesktop + ? const DesktopSignInScreen() + : const MobileSignInScreen(); }, ), ); @@ -47,7 +41,7 @@ class SignInScreen extends StatelessWidget { } }, (error) { - handleOpenWorkspaceError(context, error); + Log.error('Sign in error: $error'); }, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart index c6b2d5401c..2b11068abb 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart @@ -2,8 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -41,17 +41,11 @@ class SignInAnonymousButtonV3 extends StatelessWidget { final user = bloc.state.anonUsers.first; bloc.add(AnonUserEvent.openAnonUser(user)); }; - return ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 32), - maximumSize: const Size(double.infinity, 38), - ), - onPressed: onTap, - child: FlowyText( - text, - fontSize: 14, - color: Theme.of(context).colorScheme.onPrimary, - ), + return AFFilledTextButton.primary( + text: text, + size: AFButtonSize.l, + alignment: Alignment.center, + onTap: onTap, ); }, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index a0dda40a36..efa72e7341 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -23,6 +23,8 @@ class _ContinueWithEmailAndPasswordState final focusNode = FocusNode(); final emailKey = GlobalKey(); + bool _hasPushedContinueWithMagicLinkOrPasscodePage = false; + @override void dispose() { controller.dispose(); @@ -47,10 +49,12 @@ class _ContinueWithEmailAndPasswordState ), ); } else if (successOrFail == null && !state.isSubmitting) { - _pushContinueWithMagicLinkOrPasscodePage( - context, - controller.text, - ); + emailKey.currentState?.clearError(); + + // _pushContinueWithMagicLinkOrPasscodePage( + // context, + // controller.text, + // ); } }, child: Column( @@ -95,12 +99,21 @@ class _ContinueWithEmailAndPasswordState context .read() .add(SignInEvent.signInWithMagicLink(email: email)); + + _pushContinueWithMagicLinkOrPasscodePage( + context, + email, + ); } void _pushContinueWithMagicLinkOrPasscodePage( BuildContext context, String email, ) { + if (_hasPushedContinueWithMagicLinkOrPasscodePage) { + return; + } + final signInBloc = context.read(); // push the a continue with magic link or passcode screen @@ -111,7 +124,13 @@ class _ContinueWithEmailAndPasswordState value: signInBloc, child: ContinueWithMagicLinkOrPasscodePage( email: email, - backToLogin: () => Navigator.pop(context), + backToLogin: () { + Navigator.pop(context); + + emailKey.currentState?.clearError(); + + _hasPushedContinueWithMagicLinkOrPasscodePage = false; + }, onEnterPasscode: (passcode) => signInBloc.add( SignInEvent.signInWithPasscode( email: email, @@ -122,6 +141,8 @@ class _ContinueWithEmailAndPasswordState ), ), ); + + _hasPushedContinueWithMagicLinkOrPasscodePage = true; } // void _pushContinueWithPasswordPage( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index a0b31c2fe5..2b40502b0d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -6,7 +6,6 @@ import 'package:appflowy/user/application/anon_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -78,13 +77,16 @@ class ChangeCloudModeButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - 'Cloud', - decoration: TextDecoration.underline, - color: Colors.grey, - fontSize: 12, + final theme = AppFlowyTheme.of(context); + return AFGhostIconTextButton( + text: 'Cloud', + textColor: (context, isHovering, disabled) { + return theme.textColorScheme.secondary; + }, + size: AFButtonSize.s, + padding: EdgeInsets.symmetric( + horizontal: theme.spacing.m, + vertical: theme.spacing.xs, ), onTap: () async { await useAppFlowyBetaCloudWithURL( @@ -93,6 +95,13 @@ class ChangeCloudModeButton extends StatelessWidget { ); await runAppFlowy(); }, + iconBuilder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.settings_s, + size: Size.square(20), + color: theme.textColorScheme.secondary, + ); + }, ); } } diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig index da469610eb..d2b3d7e9b3 100644 --- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = AppFlowy PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 AppFlowy.IO. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 AppFlowy.IO. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 32b3335d0f..8923f61e1f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -191,7 +191,7 @@ class AppFlowyThemeBuilder { quaternary: colorScheme.neutral.neutral1000, quaternaryHover: colorScheme.neutral.neutral900, transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey100005, + primaryAlpha5: colorScheme.neutral.alphaGrey10005, primaryAlpha5Hover: colorScheme.neutral.alphaGrey10010, primaryAlpha80: colorScheme.neutral.alphaGrey100080, primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index fdf8c8348e..006c43d696 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "af-plugin", "anyhow", @@ -362,7 +362,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "anyhow", "futures-util", @@ -376,7 +376,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d3427ef99c..475cefb3d4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } From a44ad63230edf4eb21807568050571f576ca15e9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 14 Apr 2025 16:40:54 +0800 Subject: [PATCH 309/384] chore: display preview --- .../command_palette/folder_search_test.dart | 6 +- .../command_palette/search_result_ext.dart | 10 +- .../search_result_list_bloc.dart | 62 ++++++ .../command_palette/command_palette.dart | 6 +- ...sult_tile.dart => search_result_cell.dart} | 106 +++++++--- .../widgets/search_results_list.dart | 200 +++++------------- .../widgets/search_summary_cell.dart | 91 ++++++++ frontend/resources/translations/en.json | 4 +- .../flowy-search/src/document/handler.rs | 91 ++++---- .../flowy-search/src/entities/result.rs | 29 +-- .../flowy-search/src/folder/entities.rs | 4 +- .../flowy-search/src/folder/indexer.rs | 2 +- 12 files changed, 368 insertions(+), 243 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart rename frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/{search_result_tile.dart => search_result_cell.dart} (66%) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart index a1d7f12667..0b77a0167b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart @@ -6,7 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -49,7 +49,7 @@ void main() { // The score should be higher for "ViewOna" thus it should be shown first final secondDocumentWidget = tester .widget(find.byType(SearchResultCell).first) as SearchResultCell; - expect(secondDocumentWidget.item.data, secondDocument); + expect(secondDocumentWidget.item.displayName, secondDocument); // Change search to "ViewOne" await tester.enterText(searchFieldFinder, firstDocument); @@ -59,7 +59,7 @@ void main() { final firstDocumentWidget = tester.widget( find.byType(SearchResultCell).first, ) as SearchResultCell; - expect(firstDocumentWidget.item.data, firstDocument); + expect(firstDocumentWidget.item.displayName, firstDocument); }); testWidgets('Displaying icons in search results', (tester) async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart index 563eac8a0a..6b6ea6d5c0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -5,19 +5,19 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -extension GetIcon on SearchResponseItemPB { +extension GetIcon on ResultIconPB { Widget? getIcon() { - final iconValue = icon.value, iconType = icon.ty; + final iconValue = value, iconType = ty; if (iconType == ResultIconTypePB.Emoji) { return iconValue.isNotEmpty ? FlowyText.emoji(iconValue, fontSize: 18) : null; - } else if (icon.ty == ResultIconTypePB.Icon) { + } else if (ty == ResultIconTypePB.Icon) { if (_resultIconValueTypes.contains(iconValue)) { - return FlowySvg(icon.getViewSvg(), size: const Size.square(18)); + return FlowySvg(getViewSvg(), size: const Size.square(18)); } return RawEmojiIconWidget( - emoji: EmojiIconData(iconType.toFlowyIconType(), icon.value), + emoji: EmojiIconData(iconType.toFlowyIconType(), value), emojiSize: 18, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart new file mode 100644 index 0000000000..c77530ba77 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'search_result_list_bloc.freezed.dart'; + +class SearchResultListBloc + extends Bloc { + SearchResultListBloc() : super(SearchResultListState.initial()) { + // Register event handlers + on<_OnHoverSummary>(_onHoverSummary); + on<_OnHoverResult>(_onHoverResult); + } + + FutureOr _onHoverSummary( + _OnHoverSummary event, + Emitter emit, + ) { + emit( + state.copyWith( + hoveredSummary: event.summary, + hoveredResult: null, + ), + ); + } + + FutureOr _onHoverResult( + _OnHoverResult event, + Emitter emit, + ) { + emit( + state.copyWith( + hoveredSummary: null, + hoveredResult: event.item, + ), + ); + } +} + +@freezed +class SearchResultListEvent with _$SearchResultListEvent { + const factory SearchResultListEvent.onHoverSummary({ + required SearchSummaryPB summary, + }) = _OnHoverSummary; + const factory SearchResultListEvent.onHoverResult({ + required SearchResponseItemPB item, + }) = _OnHoverResult; +} + +@freezed +class SearchResultListState with _$SearchResultListState { + const SearchResultListState._(); + const factory SearchResultListState({ + @Default(null) SearchSummaryPB? hoveredSummary, + @Default(null) SearchResponseItemPB? hoveredResult, + }) = _SearchResultListState; + + factory SearchResultListState.initial() => const SearchResultListState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index 9a507decc6..f54fbebe14 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -135,9 +135,9 @@ class CommandPaletteModal extends StatelessWidget { alignment: Alignment.topCenter, insetPadding: const EdgeInsets.only(top: 100), constraints: const BoxConstraints( - maxHeight: 520, - maxWidth: 510, - minHeight: 420, + maxHeight: 600, + maxWidth: 800, + minHeight: 600, ), expandHeight: false, child: shortcutBuilder( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart similarity index 66% rename from frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart index 551823f08e..1bd3d0b03a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart @@ -4,6 +4,7 @@ import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; +import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -12,19 +13,18 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SearchResultCell extends StatefulWidget { const SearchResultCell({ super.key, required this.item, required this.onSelected, - required this.onHover, this.isTrashed = false, }); final SearchResponseItemPB item; final VoidCallback onSelected; - final Function(SearchResponseItemPB) onHover; final bool isTrashed; @override @@ -46,7 +46,7 @@ class _SearchResultCellState extends State { widget.onSelected(); getIt().add( ActionNavigationEvent.performAction( - action: NavigationAction(objectId: widget.item.viewId), + action: NavigationAction(objectId: widget.item.id), ), ); } @@ -58,10 +58,10 @@ class _SearchResultCellState extends State { @override Widget build(BuildContext context) { - final title = widget.item.data.orDefault( + final title = widget.item.displayName.orDefault( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), ); - final icon = widget.item.getIcon(); + final icon = widget.item.icon.getIcon(); final cleanedPreview = _cleanPreview(widget.item.preview); final hasPreview = cleanedPreview.isNotEmpty; final trashHintText = @@ -134,36 +134,45 @@ class _SearchResultCellState extends State { ); } - return MouseRegion( - onEnter: (_) => widget.onHover(widget.item), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleSelection, - child: Focus( - focusNode: focusNode, - onKeyEvent: (node, event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.enter) { - _handleSelection(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleSelection, + child: Focus( + focusNode: focusNode, + onKeyEvent: (node, event) { + if (event is! KeyDownEvent) return KeyEventResult.ignored; + if (event.logicalKey == LogicalKeyboardKey.enter) { + _handleSelection(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + onFocusChange: (hasFocus) { + setState(() { + context.read().add( + SearchResultListEvent.onHoverResult(item: widget.item), + ); + _hasFocus = hasFocus; + }); + }, + child: FlowyHover( + onHover: (value) { + context.read().add( + SearchResultListEvent.onHoverResult(item: widget.item), + ); }, - onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), - child: FlowyHover( - isSelected: () => _hasFocus, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: tileContent, - ), + isSelected: () => _hasFocus, + style: HoverStyle( + borderRadius: BorderRadius.circular(8), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: tileContent, ), ), ), @@ -192,3 +201,32 @@ class _DocumentPreview extends StatelessWidget { ); } } + +class SearchResultPreview extends StatelessWidget { + const SearchResultPreview({ + super.key, + required this.data, + }); + + final SearchResponseItemPB data; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText(LocaleKeys.commandPalette_pagePreview.tr()), + const Divider( + thickness: 1, + ), + const VSpace(6), + Expanded( + child: FlowyText( + data.content, + maxLines: 30, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index 2eb3b44b3e..5d2e305c15 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -1,13 +1,17 @@ +import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class SearchResultList extends StatefulWidget { +import 'search_result_cell.dart'; +import 'search_summary_cell.dart'; + +class SearchResultList extends StatelessWidget { const SearchResultList({ required this.trash, required this.resultItems, @@ -19,13 +23,6 @@ class SearchResultList extends StatefulWidget { final List resultItems; final List resultSummaries; - @override - State createState() => _SearchResultListState(); -} - -class _SearchResultListState extends State { - dynamic _selectedCellData; - Widget _buildSectionHeader(String title) => Padding( padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8), @@ -35,31 +32,18 @@ class _SearchResultListState extends State { ), ); - void _onHoverSummary(SearchSummaryPB summary) { - setState(() { - _selectedCellData = summary; - }); - } - - void _onHoverResult(SearchResponseItemPB item) { - setState(() { - _selectedCellData = item; - }); - } - Widget _buildSummariesSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiSummary.tr()), + _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: widget.resultSummaries.length, + itemCount: resultSummaries.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) => SearchSummaryCell( - summary: widget.resultSummaries[index], - onHover: _onHoverSummary, + summary: resultSummaries[index], ), ), ], @@ -75,15 +59,14 @@ class _SearchResultListState extends State { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: widget.resultItems.length, + itemCount: resultItems.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) { - final item = widget.resultItems[index]; + final item = resultItems[index]; return SearchResultCell( item: item, onSelected: () => FlowyOverlay.pop(context), - isTrashed: widget.trash.any((t) => t.id == item.viewId), - onHover: _onHoverResult, + isTrashed: trash.any((t) => t.id == item.id), ); }, ), @@ -95,128 +78,55 @@ class _SearchResultListState extends State { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), - child: Row( - children: [ - Flexible( - flex: 7, - child: ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - if (widget.resultSummaries.isNotEmpty) _buildSummariesSection(), - const VSpace(10), - if (widget.resultItems.isNotEmpty) - _buildResultsSection(context), - ], - ), - ), - Flexible( - flex: 3, - child: SearchCellDetail(cellData: _selectedCellData), - ), - ], - ), - ); - } -} - -class SearchCellDetail extends StatelessWidget { - const SearchCellDetail({ - super.key, - required this.cellData, - }); - - final dynamic cellData; - - @override - Widget build(BuildContext context) { - if (cellData == null) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: FlowyText( - 'Hover over an item to see details', - fontSize: 12, - color: Colors.grey, - ), - ), - ); - } - - if (cellData is SearchSummaryPB) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( + child: BlocProvider( + create: (context) => SearchResultListBloc(), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const FlowyText( - 'AI Summary', - fontSize: 14, - fontWeight: FontWeight.bold, - ), - const VSpace(8), - FlowyText( - cellData.content, - fontSize: 12, - ), - ], - ), - ); - } - - if (cellData is SearchResponseItemPB) { - final item = cellData as SearchResponseItemPB; - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - item.data, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - const VSpace(8), - if (item.preview.isNotEmpty) - FlowyText( - item.preview, - fontSize: 12, + Flexible( + flex: 7, + child: ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + if (resultSummaries.isNotEmpty) _buildSummariesSection(), + const VSpace(10), + if (resultItems.isNotEmpty) _buildResultsSection(context), + ], ), + ), + const HSpace(10), + Flexible( + flex: 3, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + child: const SearchCellPreview(), + ), + ), ], ), - ); - } - - return const SizedBox.shrink(); - } -} - -class SearchSummaryCell extends StatefulWidget { - const SearchSummaryCell({ - required this.summary, - required this.onHover, - super.key, - }); - - final SearchSummaryPB summary; - final Function(SearchSummaryPB) onHover; - - @override - State createState() => _SearchSummaryCellState(); -} - -class _SearchSummaryCellState extends State { - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => widget.onHover(widget.summary), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: FlowyText( - widget.summary.content, - maxLines: 3, - ), ), ); } } + +class SearchCellPreview extends StatelessWidget { + const SearchCellPreview({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.hoveredSummary != null) { + return SearchSummaryPreview(summary: state.hoveredSummary!); + } else if (state.hoveredResult != null) { + return SearchResultPreview(data: state.hoveredResult!); + } + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart new file mode 100644 index 0000000000..c911c46735 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -0,0 +1,91 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; +import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SearchSummaryCell extends StatelessWidget { + const SearchSummaryCell({ + required this.summary, + super.key, + }); + + final SearchSummaryPB summary; + + @override + Widget build(BuildContext context) { + return FlowyHover( + onHover: (value) { + context.read().add( + SearchResultListEvent.onHoverSummary(summary: summary), + ); + }, + style: HoverStyle( + borderRadius: BorderRadius.circular(8), + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: FlowyText( + summary.content, + maxLines: 3, + ), + ), + ); + } +} + +class SearchSummaryPreview extends StatelessWidget { + const SearchSummaryPreview({ + required this.summary, + super.key, + }); + + final SearchSummaryPB summary; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText(LocaleKeys.commandPalette_aiOverviewSource.tr()), + const Divider( + thickness: 1, + ), + const VSpace(6), + ...summary.sources.map((e) => SearchSummarySource(source: e)), + ], + ); + } +} + +class SearchSummarySource extends StatelessWidget { + const SearchSummarySource({ + required this.source, + super.key, + }); + + final SearchSourcePB source; + + @override + Widget build(BuildContext context) { + final icon = source.icon.getIcon(); + return Row( + children: [ + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + FlowyText(source.displayName), + ], + ); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 147902d799..4279f95425 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2693,7 +2693,9 @@ "commandPalette": { "placeholder": "Search or ask a question...", "bestMatches": "Best matches", - "aiSummary": "AI summary", + "aiOverview": "AI overview", + "aiOverviewSource": "Sources", + "pagePreview": "Preview", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index b808b38e74..9077829c07 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,6 +1,6 @@ use crate::entities::{ CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB, - SearchSummaryPB, + SearchSourcePB, SearchSummaryPB, }; use crate::{ entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, @@ -8,6 +8,7 @@ use crate::{ }; use async_stream::stream; use flowy_error::FlowyResult; +use flowy_folder::entities::ViewPB; use flowy_folder::{manager::FolderManager, ViewLayout}; use flowy_search_pub::cloud::{SearchCloudService, SearchResult}; use lib_infra::async_trait::async_trait; @@ -34,7 +35,6 @@ impl DocumentSearchHandler { } } } - #[async_trait] impl SearchHandler for DocumentSearchHandler { fn search_type(&self) -> SearchType { @@ -50,14 +50,15 @@ impl SearchHandler for DocumentSearchHandler { let folder_manager = self.folder_manager.clone(); Box::pin(stream! { - let filter = match filter { - Some(f) => f, - None => { - yield Ok(CreateSearchResultPBArgs::default().build().unwrap()); - return; - }, + // Exit early if there is no filter. + let filter = if let Some(f) = filter { + f + } else { + yield Ok(CreateSearchResultPBArgs::default().build().unwrap()); + return; }; + // Parse workspace id. let workspace_id = match Uuid::from_str(&filter.workspace_id) { Ok(id) => id, Err(e) => { @@ -66,62 +67,54 @@ impl SearchHandler for DocumentSearchHandler { } }; + // Retrieve all available views. let views = match folder_manager.get_all_views_pb().await { Ok(views) => views, Err(e) => { yield Err(e); return; - }, + } }; + // Execute document search. let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await { Ok(items) => items, Err(e) => { yield Err(e); return; - }, + } }; - let summary_input = result_items + // Prepare input for search summary generation. + let summary_input: Vec = result_items .iter() .map(|v| SearchResult { object_id: v.object_id, content: v.content.clone(), }) - .collect::>(); + .collect(); + // Build search response items. let mut items: Vec = Vec::new(); - for item in result_items.iter() { + for item in &result_items { if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) { - let icon: Option = match view.icon.clone() { - Some(view_icon) => Some(ResultIconPB::from(view_icon)), - None => { - let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); - Some(ResultIconPB { - ty: ResultIconTypePB::Icon, - value: view_layout_ty.to_string(), - }) - }, - }; - items.push(SearchResponseItemPB { index_type: IndexTypePB::Document, - view_id: item.object_id.to_string(), id: item.object_id.to_string(), - data: view.name.clone(), - icon, + display_name: view.name.clone(), + icon: extract_icon(view), score: item.score, workspace_id: item.workspace_id.to_string(), preview: item.preview.clone(), - }); + content: item.content.clone()} + ); } else { warn!("No view found for search result: {:?}", item); } } - let search_result = RepeatedSearchResponseItemPB { - items, - }; + // Yield primary search result. + let search_result = RepeatedSearchResponseItemPB { items }; yield Ok( CreateSearchResultPBArgs::default() .search_result(Some(search_result)) @@ -129,7 +122,7 @@ impl SearchHandler for DocumentSearchHandler { .unwrap(), ); - // Search summary generation. + // Generate and yield search summary. match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await { Ok(summary_result) => { trace!("[Search] search summary: {:?}", summary_result); @@ -137,13 +130,26 @@ impl SearchHandler for DocumentSearchHandler { .summaries .into_iter() .map(|v| { - SearchSummaryPB { content: v.content, source_ids: v.sources.iter().map(|id| id.to_string()).collect() } + let sources: Vec = v.sources + .iter() + .flat_map(|id| { + if let Some(view) = views.iter().find(|v| v.id == id.to_string()) { + Some(SearchSourcePB { + id: id.to_string(), + display_name: view.name.clone(), + icon: extract_icon(view), + }) + } else { + None + } + }) + .collect(); + + SearchSummaryPB { content: v.content, sources } }) .collect(); - let summary_result = RepeatedSearchSummaryPB { - items: summaries, - }; + let summary_result = RepeatedSearchSummaryPB { items: summaries }; yield Ok( CreateSearchResultPBArgs::default() .search_summary(Some(summary_result)) @@ -158,3 +164,16 @@ impl SearchHandler for DocumentSearchHandler { }) } } + +fn extract_icon(view: &ViewPB) -> Option { + match view.icon.clone() { + Some(view_icon) => Some(ResultIconPB::from(view_icon)), + None => { + let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); + Some(ResultIconPB { + ty: ResultIconTypePB::Icon, + value: view_layout_ty.to_string(), + }) + }, + } +} diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index 48de24728d..28f4c0111f 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -29,7 +29,7 @@ pub struct SearchSummaryPB { pub content: String, #[pb(index = 2)] - pub source_ids: Vec, + pub sources: Vec, } #[derive(ProtoBuf, Default, Debug, Clone)] @@ -38,7 +38,10 @@ pub struct SearchSourcePB { pub id: String, #[pb(index = 2)] - pub source: String, + pub display_name: String, + + #[pb(index = 3, one_of)] + pub icon: Option, } #[derive(ProtoBuf, Default, Debug, Clone)] @@ -53,38 +56,38 @@ pub struct SearchResponseItemPB { pub index_type: IndexTypePB, #[pb(index = 2)] - pub view_id: String, - - #[pb(index = 3)] pub id: String, - #[pb(index = 4)] - pub data: String, + #[pb(index = 3)] + pub display_name: String, - #[pb(index = 5, one_of)] + #[pb(index = 4, one_of)] pub icon: Option, - #[pb(index = 6)] + #[pb(index = 5)] pub score: f64, - #[pb(index = 7)] + #[pb(index = 6)] pub workspace_id: String, - #[pb(index = 8, one_of)] + #[pb(index = 7, one_of)] pub preview: Option, + + #[pb(index = 8)] + pub content: String, } impl SearchResponseItemPB { pub fn with_score(&self, score: f64) -> Self { SearchResponseItemPB { index_type: self.index_type.clone(), - view_id: self.view_id.clone(), id: self.id.clone(), - data: self.data.clone(), + display_name: self.display_name.clone(), icon: self.icon.clone(), score, workspace_id: self.workspace_id.clone(), preview: self.preview.clone(), + content: self.content.clone(), } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index 98f6c469ca..1e9fb1f0d9 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -24,13 +24,13 @@ impl From for SearchResponseItemPB { Self { index_type: IndexTypePB::View, - view_id: data.id.clone(), id: data.id, - data: data.title, + display_name: data.title, score: 0.0, icon, workspace_id: data.workspace_id, preview: None, + content: "".to_string(), } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 5de540b0f2..9875c02465 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -158,7 +158,7 @@ impl FolderIndexManagerImpl { if !content.is_empty() { let s = serde_json::to_string(&content)?; let result: SearchResponseItemPB = serde_json::from_str::(&s)?.into(); - results.push(result.with_score(self.score_result(&query, &result.data) as f64)); + results.push(result.with_score(self.score_result(&query, &result.display_name))); } } From 35bc0957604c670738b22ef884aae9dd3769fdbe Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 14 Apr 2025 22:05:21 +0800 Subject: [PATCH 310/384] chore: local and server result --- .../command_palette/recent_history_test.dart | 7 +- .../command_palette/command_palette_bloc.dart | 268 ++++++++++-------- .../search_result_list_bloc.dart | 20 +- .../command_palette/search_service.dart | 42 ++- .../command_palette/command_palette.dart | 4 +- .../widgets/recent_views_list.dart | 4 +- ...tile.dart => search_recent_view_cell.dart} | 4 +- .../widgets/search_result_cell.dart | 22 +- .../widgets/search_results_list.dart | 71 +++-- .../widgets/search_summary_cell.dart | 23 +- frontend/resources/translations/en.json | 4 +- .../flowy-ai/src/local_ai/resource.rs | 4 +- .../flowy-search/src/document/handler.rs | 17 +- .../flowy-search/src/entities/index_type.rs | 31 -- .../rust-lib/flowy-search/src/entities/mod.rs | 2 - .../flowy-search/src/entities/notification.rs | 6 +- .../flowy-search/src/entities/result.rs | 56 ++-- .../flowy-search/src/folder/entities.rs | 8 +- .../flowy-search/src/folder/handler.rs | 8 +- .../flowy-search/src/folder/indexer.rs | 27 +- .../flowy-search/src/services/manager.rs | 32 +-- 21 files changed, 346 insertions(+), 314 deletions(-) rename frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/{recent_view_tile.dart => search_recent_view_cell.dart} (94%) delete mode 100644 frontend/rust-lib/flowy-search/src/entities/index_type.rs diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart index 277ae8f21e..b9495ae0e7 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -27,11 +27,12 @@ void main() { expect(find.byType(RecentViewsList), findsOneWidget); // Expect three recent history items - expect(find.byType(RecentViewTile), findsNWidgets(3)); + expect(find.byType(SearchRecentViewCell), findsNWidgets(3)); // Expect the first item to be the last viewed document final firstDocumentWidget = - tester.widget(find.byType(RecentViewTile).first) as RecentViewTile; + tester.widget(find.byType(SearchRecentViewCell).first) + as SearchRecentViewCell; expect(firstDocumentWidget.view.name, secondDocument); }); }); diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index b01402c868..b53f3f8c23 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -11,10 +11,25 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'command_palette_bloc.freezed.dart'; +class Debouncer { + Debouncer({required this.delay}); + + final Duration delay; + Timer? _timer; + + void run(void Function() action) { + _timer?.cancel(); + _timer = Timer(delay, action); + } + + void dispose() { + _timer?.cancel(); + } +} + class CommandPaletteBloc extends Bloc { CommandPaletteBloc() : super(CommandPaletteState.initial()) { - // Register event handlers on<_SearchChanged>(_onSearchChanged); on<_PerformSearch>(_onPerformSearch); on<_NewSearchStream>(_onNewSearchStream); @@ -26,38 +41,35 @@ class CommandPaletteBloc _initTrash(); } - Timer? _debounceOnChanged; + final Debouncer _searchDebouncer = Debouncer( + delay: const Duration(milliseconds: 300), + ); final TrashService _trashService = TrashService(); final TrashListener _trashListener = TrashListener(); - String? _oldQuery; + String? _activeQuery; String? _workspaceId; @override Future close() { _trashListener.close(); - _debounceOnChanged?.cancel(); + _searchDebouncer.dispose(); state.searchResponseStream?.dispose(); return super.close(); } Future _initTrash() async { - // Start listening for trash updates _trashListener.start( - trashUpdated: (trashOrFailed) { - add( - CommandPaletteEvent.trashChanged( - trash: trashOrFailed.toNullable(), - ), - ); - }, + trashUpdated: (trashOrFailed) => add( + CommandPaletteEvent.trashChanged( + trash: trashOrFailed.toNullable(), + ), + ), ); - // Read initial trash state and forward results final trashOrFailure = await _trashService.readTrash(); - add( - CommandPaletteEvent.trashChanged( - trash: trashOrFailure.toNullable()?.items, - ), + trashOrFailure.fold( + (trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)), + (error) => debugPrint('Failed to load trash: $error'), ); } @@ -65,9 +77,7 @@ class CommandPaletteBloc _SearchChanged event, Emitter emit, ) { - _debounceOnChanged?.cancel(); - _debounceOnChanged = Timer( - const Duration(milliseconds: 300), + _searchDebouncer.run( () { if (!isClosed) { add(CommandPaletteEvent.performSearch(search: event.search)); @@ -80,31 +90,44 @@ class CommandPaletteBloc _PerformSearch event, Emitter emit, ) async { - if (event.search.isNotEmpty && event.search != state.query) { - _oldQuery = state.query; + if (event.search.isEmpty && event.search != state.query) { + emit( + state.copyWith( + query: null, + isLoading: false, + serverResponseItems: [], + localResponseItems: [], + combinedResponseItems: {}, + resultSummaries: [], + ), + ); + } else { emit(state.copyWith(query: event.search, isLoading: true)); + _activeQuery = event.search; - // Fire off search asynchronously (fire and forget) unawaited( SearchBackendService.performSearch( event.search, workspaceId: _workspaceId, ).then( - (result) => result.onSuccess((stream) { - if (!isClosed) { - add(CommandPaletteEvent.newSearchStream(stream: stream)); - } - }), - ), - ); - } else { - // Clear state if search is empty or unchanged - emit( - state.copyWith( - query: null, - isLoading: false, - resultItems: [], - resultSummaries: [], + (result) => result.fold( + (stream) { + if (!isClosed && _activeQuery == event.search) { + add(CommandPaletteEvent.newSearchStream(stream: stream)); + } + }, + (error) { + debugPrint('Search error: $error'); + if (!isClosed) { + add( + CommandPaletteEvent.resultsChanged( + searchId: '', + isLoading: false, + ), + ); + } + }, + ), ), ); } @@ -123,83 +146,88 @@ class CommandPaletteBloc ); event.stream.listen( - onItems: ( - List items, - String searchId, - bool isLoading, - ) { - if (_isActiveSearch(searchId)) { - add( - CommandPaletteEvent.resultsChanged( - items: items, - searchId: searchId, - isLoading: isLoading, - ), - ); - } - }, - onSummaries: ( - List summaries, - String searchId, - bool isLoading, - ) { - if (_isActiveSearch(searchId)) { - add( - CommandPaletteEvent.resultsChanged( - summaries: summaries, - searchId: searchId, - isLoading: isLoading, - ), - ); - } - }, - onFinished: (String searchId) { - if (_isActiveSearch(searchId)) { - add( - CommandPaletteEvent.resultsChanged( - searchId: searchId, - isLoading: false, - ), - ); - } - }, + onLocalItems: (items, searchId) => _handleResultsUpdate( + searchId: searchId, + localItems: items, + ), + onServerItems: (items, searchId, isLoading) => _handleResultsUpdate( + searchId: searchId, + serverItems: items, + isLoading: isLoading, + ), + onSummaries: (summaries, searchId, isLoading) => _handleResultsUpdate( + searchId: searchId, + summaries: summaries, + isLoading: isLoading, + ), + onFinished: (searchId) => _handleResultsUpdate( + searchId: searchId, + isLoading: false, + ), ); } + void _handleResultsUpdate({ + required String searchId, + List? serverItems, + List? localItems, + List? summaries, + bool isLoading = true, + }) { + if (_isActiveSearch(searchId)) { + add( + CommandPaletteEvent.resultsChanged( + searchId: searchId, + serverItems: serverItems, + localItems: localItems, + summaries: summaries, + isLoading: isLoading, + ), + ); + } + } + FutureOr _onResultsChanged( _ResultsChanged event, Emitter emit, ) async { - // If query was updated since last emission, clear previous results. - if (state.query != _oldQuery) { - emit( - state.copyWith( - resultItems: [], - resultSummaries: [], - isLoading: event.isLoading, - ), - ); - _oldQuery = state.query; - } - - // Check for outdated search streams if (state.searchId != event.searchId) return; - final updatedItems = - event.items ?? List.from(state.resultItems); - final updatedSummaries = - event.summaries ?? List.from(state.resultSummaries); + final combinedItems = {}; + for (final item in event.serverItems ?? state.serverResponseItems) { + combinedItems[item.id] = SearchResultItem( + id: item.id, + icon: item.icon, + displayName: item.displayName, + content: item.content, + workspaceId: item.workspaceId, + ); + } + + for (final item in event.localItems ?? state.localResponseItems) { + combinedItems.putIfAbsent( + item.id, + () => SearchResultItem( + id: item.id, + icon: item.icon, + displayName: item.displayName, + content: '', + workspaceId: item.workspaceId, + ), + ); + } emit( state.copyWith( - resultItems: updatedItems, - resultSummaries: updatedSummaries, + serverResponseItems: event.serverItems ?? state.serverResponseItems, + localResponseItems: event.localItems ?? state.localResponseItems, + resultSummaries: event.summaries ?? state.resultSummaries, + combinedResponseItems: combinedItems, isLoading: event.isLoading, ), ); } - // Update trash state and, in case of null, retry reading trash from the service FutureOr _onTrashChanged( _TrashChanged event, Emitter emit, @@ -216,7 +244,6 @@ class CommandPaletteBloc } } - // Update the workspace and clear current search results and query FutureOr _onWorkspaceChanged( _WorkspaceChanged event, Emitter emit, @@ -225,27 +252,20 @@ class CommandPaletteBloc emit( state.copyWith( query: '', - resultItems: [], + serverResponseItems: [], + localResponseItems: [], + combinedResponseItems: {}, resultSummaries: [], isLoading: false, ), ); } - // Clear search state FutureOr _onClearSearch( _ClearSearch event, Emitter emit, ) { - emit( - state.copyWith( - query: '', - resultItems: [], - resultSummaries: [], - isLoading: false, - searchId: null, - ), - ); + emit(CommandPaletteState.initial().copyWith(trash: state.trash)); } bool _isActiveSearch(String searchId) => @@ -264,7 +284,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.resultsChanged({ required String searchId, required bool isLoading, - List? items, + List? serverItems, + List? localItems, List? summaries, }) = _ResultsChanged; @@ -277,12 +298,30 @@ class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.clearSearch() = _ClearSearch; } +class SearchResultItem { + const SearchResultItem({ + required this.id, + required this.icon, + required this.content, + required this.displayName, + this.workspaceId, + }); + + final String id; + final String content; + final ResultIconPB icon; + final String displayName; + final String? workspaceId; +} + @freezed class CommandPaletteState with _$CommandPaletteState { const CommandPaletteState._(); const factory CommandPaletteState({ @Default(null) String? query, - @Default([]) List resultItems, + @Default([]) List serverResponseItems, + @Default([]) List localResponseItems, + @Default({}) Map combinedResponseItems, @Default([]) List resultSummaries, @Default(null) SearchResponseStream? searchResponseStream, required bool isLoading, @@ -290,6 +329,7 @@ class CommandPaletteState with _$CommandPaletteState { @Default(null) String? searchId, }) = _CommandPaletteState; - factory CommandPaletteState.initial() => - const CommandPaletteState(isLoading: false); + factory CommandPaletteState.initial() => const CommandPaletteState( + isLoading: false, + ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart index c77530ba77..58e5a951da 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; @@ -13,6 +14,7 @@ class SearchResultListBloc // Register event handlers on<_OnHoverSummary>(_onHoverSummary); on<_OnHoverResult>(_onHoverResult); + on<_OpenPage>(_onOpenPage); } FutureOr _onHoverSummary( @@ -23,6 +25,7 @@ class SearchResultListBloc state.copyWith( hoveredSummary: event.summary, hoveredResult: null, + openPageId: null, ), ); } @@ -35,9 +38,17 @@ class SearchResultListBloc state.copyWith( hoveredSummary: null, hoveredResult: event.item, + openPageId: null, ), ); } + + FutureOr _onOpenPage( + _OpenPage event, + Emitter emit, + ) { + emit(state.copyWith(openPageId: event.pageId)); + } } @freezed @@ -46,8 +57,12 @@ class SearchResultListEvent with _$SearchResultListEvent { required SearchSummaryPB summary, }) = _OnHoverSummary; const factory SearchResultListEvent.onHoverResult({ - required SearchResponseItemPB item, + required SearchResultItem item, }) = _OnHoverResult; + + const factory SearchResultListEvent.openPage({ + required String pageId, + }) = _OpenPage; } @freezed @@ -55,7 +70,8 @@ class SearchResultListState with _$SearchResultListState { const SearchResultListState._(); const factory SearchResultListState({ @Default(null) SearchSummaryPB? hoveredSummary, - @Default(null) SearchResponseItemPB? hoveredResult, + @Default(null) SearchResultItem? hoveredResult, + @Default(null) String? openPageId, }) = _SearchResultListState; factory SearchResultListState.initial() => const SearchResultListState(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart index 6b862bc9ab..6f05b88081 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -50,12 +50,18 @@ class SearchResponseStream { List items, String searchId, bool isLoading, - )? _onItems; + )? _onServerItems; void Function( List summaries, String searchId, bool isLoading, )? _onSummaries; + + void Function( + List items, + String searchId, + )? _onLocalItems; + void Function(String searchId)? _onFinished; int get nativePort => _port.sendPort.nativePort; @@ -65,21 +71,28 @@ class SearchResponseStream { } void _onResultsChanged(Uint8List data) { - final response = SearchResponsePB.fromBuffer(data); + final searchState = SearchStatePB.fromBuffer(data); - if (response.hasResult()) { - if (response.result.hasSearchResult()) { - _onItems?.call( - response.result.searchResult.items, + if (searchState.hasResponse()) { + if (searchState.response.hasSearchResult()) { + _onServerItems?.call( + searchState.response.searchResult.items, searchId, - response.isLoading, + searchState.isLoading, ); } - if (response.result.hasSearchSummary()) { + if (searchState.response.hasSearchSummary()) { _onSummaries?.call( - response.result.searchSummary.items, + searchState.response.searchSummary.items, + searchId, + searchState.isLoading, + ); + } + + if (searchState.response.hasLocalSearchResult()) { + _onLocalItems?.call( + searchState.response.localSearchResult.items, searchId, - response.isLoading, ); } } else { @@ -92,16 +105,21 @@ class SearchResponseStream { List items, String searchId, bool isLoading, - )? onItems, + )? onServerItems, required void Function( List summaries, String searchId, bool isLoading, )? onSummaries, + required void Function( + List items, + String searchId, + )? onLocalItems, required void Function(String searchId)? onFinished, }) { - _onItems = onItems; + _onServerItems = onServerItems; _onSummaries = onSummaries; + _onLocalItems = onLocalItems; _onFinished = onFinished; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index f54fbebe14..a08c46679c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -153,13 +153,13 @@ class CommandPaletteModal extends StatelessWidget { ), ), ], - if (state.resultItems.isNotEmpty && + if (state.combinedResponseItems.isNotEmpty && (state.query?.isNotEmpty ?? false)) ...[ const Divider(height: 0), Flexible( child: SearchResultList( trash: state.trash, - resultItems: state.resultItems, + resultItems: state.combinedResponseItems.values.toList(), resultSummaries: state.resultSummaries, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index b0f87005d2..3bc160ee81 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -52,7 +52,7 @@ class RecentViewsList extends StatelessWidget { ) : FlowySvg(view.iconData, size: const Size.square(20)); - return RecentViewTile( + return SearchRecentViewCell( icon: SizedBox(width: 24, child: icon), view: view, onSelected: onSelected, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart similarity index 94% rename from frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart index 645b9696c8..a803f9b44c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart @@ -7,8 +7,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -class RecentViewTile extends StatelessWidget { - const RecentViewTile({ +class SearchRecentViewCell extends StatelessWidget { + const SearchRecentViewCell({ super.key, required this.icon, required this.view, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart index 1bd3d0b03a..7034b79821 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart @@ -1,11 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -19,12 +16,10 @@ class SearchResultCell extends StatefulWidget { const SearchResultCell({ super.key, required this.item, - required this.onSelected, this.isTrashed = false, }); - final SearchResponseItemPB item; - final VoidCallback onSelected; + final SearchResultItem item; final bool isTrashed; @override @@ -43,12 +38,9 @@ class _SearchResultCellState extends State { /// Helper to handle the selection action. void _handleSelection() { - widget.onSelected(); - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: widget.item.id), - ), - ); + context.read().add( + SearchResultListEvent.openPage(pageId: widget.item.id), + ); } /// Helper to clean up preview text. @@ -62,7 +54,7 @@ class _SearchResultCellState extends State { LocaleKeys.menuAppHeader_defaultNewPageName.tr(), ); final icon = widget.item.icon.getIcon(); - final cleanedPreview = _cleanPreview(widget.item.preview); + final cleanedPreview = _cleanPreview(widget.item.content); final hasPreview = cleanedPreview.isNotEmpty; final trashHintText = widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null; @@ -208,7 +200,7 @@ class SearchResultPreview extends StatelessWidget { required this.data, }); - final SearchResponseItemPB data; + final SearchResultItem data; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index 5d2e305c15..af2944d09c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -1,3 +1,7 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:flutter/material.dart'; @@ -20,9 +24,8 @@ class SearchResultList extends StatelessWidget { }); final List trash; - final List resultItems; + final List resultItems; final List resultSummaries; - Widget _buildSectionHeader(String title) => Padding( padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8), @@ -65,7 +68,6 @@ class SearchResultList extends StatelessWidget { final item = resultItems[index]; return SearchResultCell( item: item, - onSelected: () => FlowyOverlay.pop(context), isTrashed: trash.any((t) => t.id == item.id), ); }, @@ -80,33 +82,46 @@ class SearchResultList extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6), child: BlocProvider( create: (context) => SearchResultListBloc(), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - flex: 7, - child: ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - if (resultSummaries.isNotEmpty) _buildSummariesSection(), - const VSpace(10), - if (resultItems.isNotEmpty) _buildResultsSection(context), - ], - ), - ), - const HSpace(10), - Flexible( - flex: 3, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 16, + child: BlocListener( + listener: (context, state) { + if (state.openPageId != null) { + FlowyOverlay.pop(context); + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: state.openPageId!), + ), + ); + } + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 7, + child: ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + if (resultSummaries.isNotEmpty) _buildSummariesSection(), + const VSpace(10), + if (resultItems.isNotEmpty) _buildResultsSection(context), + ], ), - child: const SearchCellPreview(), ), - ), - ], + const HSpace(10), + if (resultItems.any((item) => item.content.isNotEmpty)) + Flexible( + flex: 3, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + child: const SearchCellPreview(), + ), + ), + ], + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart index c911c46735..15e8bb18f7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -36,7 +36,7 @@ class SearchSummaryCell extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: FlowyText( summary.content, - maxLines: 3, + maxLines: 20, ), ), ); @@ -78,14 +78,19 @@ class SearchSummarySource extends StatelessWidget { @override Widget build(BuildContext context) { final icon = source.icon.getIcon(); - return Row( - children: [ - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - FlowyText(source.displayName), - ], + return SizedBox( + height: 30, + child: FlowyButton( + leftIcon: icon, + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + text: FlowyText(source.displayName), + onTap: () { + context.read().add( + SearchResultListEvent.openPage(pageId: source.id), + ); + }, + ), ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4279f95425..21c39c63f5 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2694,8 +2694,8 @@ "placeholder": "Search or ask a question...", "bestMatches": "Best matches", "aiOverview": "AI overview", - "aiOverviewSource": "Sources", - "pagePreview": "Preview", + "aiOverviewSource": "Reference sources", + "pagePreview": "Content preview", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 2fc6fad2cc..172204746a 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -71,10 +71,10 @@ impl LocalAIResourceController { ) -> Self { let (resource_notify, _) = tokio::sync::broadcast::channel(1); let (app_state_sender, _) = tokio::sync::broadcast::channel(1); - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "linux"))] let mut offline_app_disk_watch: Option = None; - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "linux"))] { match watch_offline_app() { Ok((new_watcher, mut rx)) => { diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index 9077829c07..fc68f850e5 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,9 +1,9 @@ use crate::entities::{ - CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB, - SearchSourcePB, SearchSummaryPB, + CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, + SearchResponsePB, SearchSourcePB, SearchSummaryPB, }; use crate::{ - entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, + entities::{ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, services::manager::{SearchHandler, SearchType}, }; use async_stream::stream; @@ -45,7 +45,7 @@ impl SearchHandler for DocumentSearchHandler { &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>> { + ) -> Pin> + Send + 'static>> { let cloud_service = self.cloud_service.clone(); let folder_manager = self.folder_manager.clone(); @@ -99,13 +99,10 @@ impl SearchHandler for DocumentSearchHandler { for item in &result_items { if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) { items.push(SearchResponseItemPB { - index_type: IndexTypePB::Document, id: item.object_id.to_string(), display_name: view.name.clone(), icon: extract_icon(view), - score: item.score, workspace_id: item.workspace_id.to_string(), - preview: item.preview.clone(), content: item.content.clone()} ); } else { @@ -133,15 +130,11 @@ impl SearchHandler for DocumentSearchHandler { let sources: Vec = v.sources .iter() .flat_map(|id| { - if let Some(view) = views.iter().find(|v| v.id == id.to_string()) { - Some(SearchSourcePB { + views.iter().find(|v| v.id == id.to_string()).map(|view| SearchSourcePB { id: id.to_string(), display_name: view.name.clone(), icon: extract_icon(view), }) - } else { - None - } }) .collect(); diff --git a/frontend/rust-lib/flowy-search/src/entities/index_type.rs b/frontend/rust-lib/flowy-search/src/entities/index_type.rs deleted file mode 100644 index 77adc76a97..0000000000 --- a/frontend/rust-lib/flowy-search/src/entities/index_type.rs +++ /dev/null @@ -1,31 +0,0 @@ -use flowy_derive::ProtoBuf_Enum; - -#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] -pub enum IndexTypePB { - View = 0, - Document = 1, - DocumentBlock = 2, - DatabaseRow = 3, -} - -impl Default for IndexTypePB { - fn default() -> Self { - Self::View - } -} - -impl std::convert::From for i32 { - fn from(notification: IndexTypePB) -> Self { - notification as i32 - } -} - -impl std::convert::From for IndexTypePB { - fn from(notification: i32) -> Self { - match notification { - 1 => IndexTypePB::View, - 2 => IndexTypePB::DocumentBlock, - _ => IndexTypePB::DatabaseRow, - } - } -} diff --git a/frontend/rust-lib/flowy-search/src/entities/mod.rs b/frontend/rust-lib/flowy-search/src/entities/mod.rs index b4d7c682b9..dc6aaace08 100644 --- a/frontend/rust-lib/flowy-search/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-search/src/entities/mod.rs @@ -1,10 +1,8 @@ -mod index_type; mod notification; mod query; mod result; mod search_filter; -pub use index_type::*; pub use notification::*; pub use query::*; pub use result::*; diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index 0a51eb85d3..3f1cdab67a 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -1,10 +1,10 @@ -use super::SearchResultPB; +use super::SearchResponsePB; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchResponsePB { +pub struct SearchStatePB { #[pb(index = 1, one_of)] - pub result: Option, + pub response: Option, #[pb(index = 2)] pub search_id: String, diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index 28f4c0111f..8f5ba11ded 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -1,4 +1,3 @@ -use super::IndexTypePB; use collab_folder::{IconType, ViewIcon}; use derive_builder::Builder; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; @@ -7,7 +6,7 @@ use flowy_folder::entities::ViewIconPB; #[derive(Debug, Default, ProtoBuf, Builder, Clone)] #[builder(name = "CreateSearchResultPBArgs")] #[builder(pattern = "mutable")] -pub struct SearchResultPB { +pub struct SearchResponsePB { #[pb(index = 1, one_of)] #[builder(default)] pub search_result: Option, @@ -15,6 +14,10 @@ pub struct SearchResultPB { #[pb(index = 2, one_of)] #[builder(default)] pub search_summary: Option, + + #[pb(index = 3, one_of)] + #[builder(default)] + pub local_search_result: Option, } #[derive(ProtoBuf, Default, Debug, Clone)] @@ -53,43 +56,40 @@ pub struct RepeatedSearchResponseItemPB { #[derive(ProtoBuf, Default, Debug, Clone)] pub struct SearchResponseItemPB { #[pb(index = 1)] - pub index_type: IndexTypePB, - - #[pb(index = 2)] pub id: String, - #[pb(index = 3)] + #[pb(index = 2)] pub display_name: String, - #[pb(index = 4, one_of)] + #[pb(index = 3, one_of)] pub icon: Option, - #[pb(index = 5)] - pub score: f64, - - #[pb(index = 6)] + #[pb(index = 4)] pub workspace_id: String, - #[pb(index = 7, one_of)] - pub preview: Option, - - #[pb(index = 8)] + #[pb(index = 5)] pub content: String, } -impl SearchResponseItemPB { - pub fn with_score(&self, score: f64) -> Self { - SearchResponseItemPB { - index_type: self.index_type.clone(), - id: self.id.clone(), - display_name: self.display_name.clone(), - icon: self.icon.clone(), - score, - workspace_id: self.workspace_id.clone(), - preview: self.preview.clone(), - content: self.content.clone(), - } - } +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct RepeatedLocalSearchResponseItemPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct LocalSearchResponseItemPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub display_name: String, + + #[pb(index = 3, one_of)] + pub icon: Option, + + #[pb(index = 4)] + pub workspace_id: String, } #[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)] diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index 1e9fb1f0d9..1bb763b4a6 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB}; +use crate::entities::{LocalSearchResponseItemPB, ResultIconPB}; #[derive(Debug, Serialize, Deserialize)] pub struct FolderIndexData { @@ -11,7 +11,7 @@ pub struct FolderIndexData { pub workspace_id: String, } -impl From for SearchResponseItemPB { +impl From for LocalSearchResponseItemPB { fn from(data: FolderIndexData) -> Self { let icon = if data.icon.is_empty() { None @@ -23,14 +23,10 @@ impl From for SearchResponseItemPB { }; Self { - index_type: IndexTypePB::View, id: data.id, display_name: data.title, - score: 0.0, icon, workspace_id: data.workspace_id, - preview: None, - content: "".to_string(), } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs index 975609a227..e21ce1c98c 100644 --- a/frontend/rust-lib/flowy-search/src/folder/handler.rs +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -1,6 +1,6 @@ use super::indexer::FolderIndexManagerImpl; use crate::entities::{ - CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB, + CreateSearchResultPBArgs, RepeatedLocalSearchResponseItemPB, SearchFilterPB, SearchResponsePB, }; use crate::services::manager::{SearchHandler, SearchType}; use async_stream::stream; @@ -30,7 +30,7 @@ impl SearchHandler for FolderSearchHandler { &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>> { + ) -> Pin> + Send + 'static>> { let index_manager = self.index_manager.clone(); Box::pin(stream! { @@ -48,8 +48,8 @@ impl SearchHandler for FolderSearchHandler { } // Build the search result. - let search_result = RepeatedSearchResponseItemPB {items}; - yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap()) + let search_result = RepeatedLocalSearchResponseItemPB {items}; + yield Ok(CreateSearchResultPBArgs::default().local_search_result(Some(search_result)).build().unwrap()) }) } } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 9875c02465..641c1f5f96 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -1,9 +1,6 @@ -use crate::{ - entities::SearchResponseItemPB, - folder::schema::{ - FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, - FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, - }, +use crate::folder::schema::{ + FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, + FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, }; use collab::core::collab::{IndexContent, IndexContentReceiver}; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexContent, ViewLayout}; @@ -14,9 +11,8 @@ use std::sync::{Arc, Weak}; use std::{collections::HashMap, fs}; use super::entities::FolderIndexData; -use crate::entities::ResultIconTypePB; +use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB}; use lib_infra::async_trait::async_trait; -use strsim::levenshtein; use tantivy::{ collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, Index, IndexReader, IndexWriter, TantivyDocument, Term, @@ -113,11 +109,6 @@ impl FolderIndexManagerImpl { (icon, icon_ty) } - fn score_result(&self, query: &str, term: &str) -> f64 { - let distance = levenshtein(query, term) as f64; - 1.0 / (distance + 1.0) - } - /// Simple implementation to index all given data by spawning async tasks. fn index_all(&self, data_vec: Vec) -> Result<(), FlowyError> { for data in data_vec { @@ -130,7 +121,7 @@ impl FolderIndexManagerImpl { } /// Searches the index using the given query string. - pub async fn search(&self, query: String) -> Result, FlowyError> { + pub async fn search(&self, query: String) -> Result, FlowyError> { let lock = self.state.read().await; let state = lock .as_ref() @@ -157,8 +148,8 @@ impl FolderIndexManagerImpl { } if !content.is_empty() { let s = serde_json::to_string(&content)?; - let result: SearchResponseItemPB = serde_json::from_str::(&s)?.into(); - results.push(result.with_score(self.score_result(&query, &result.display_name))); + let result: LocalSearchResponseItemPB = serde_json::from_str::(&s)?.into(); + results.push(result); } } @@ -200,11 +191,11 @@ impl IndexManager for FolderIndexManagerImpl { }) .await; }, - Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err), + Err(err) => error!("FolderIndexManager error deserialize (update): {:?}", err), }, IndexContent::Delete(ids) => { if let Err(e) = indexer.remove_indices(ids).await { - tracing::error!("FolderIndexManager error (delete): {:?}", e); + error!("FolderIndexManager error (delete): {:?}", e); } }, } diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index 157781eceb..72a3c12793 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -1,4 +1,4 @@ -use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB}; +use crate::entities::{SearchFilterPB, SearchResponsePB, SearchStatePB}; use allo_isolate::Isolate; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; @@ -25,7 +25,7 @@ pub trait SearchHandler: Send + Sync + 'static { &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>>; + ) -> Pin> + Send + 'static>>; } /// The [SearchManager] is used to inject multiple [SearchHandler]'s @@ -34,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static { /// pub struct SearchManager { pub handlers: HashMap>, - current_search: Arc>>, // Track current search + current_search: Arc>>, } impl SearchManager { @@ -84,23 +84,21 @@ impl SearchManager { } let mut stream = handler.perform_search(query.clone(), filter).await; - while let Some(result) = stream.next().await { + while let Some(Ok(search_result)) = stream.next().await { if !is_current_search(¤t_search, &search_id).await { trace!("[Search] discard search stream: {}", query); return; } - if let Ok(result) = result { - let resp = SearchResponsePB { - result: Some(result), - search_id: search_id.clone(), - is_loading: true, - }; - if let Ok::, _>(data) = resp.try_into() { - if let Err(err) = clone_sink.send(data).await { - error!("Failed to send search result: {}", err); - break; - } + let resp = SearchStatePB { + response: Some(search_result), + search_id: search_id.clone(), + is_loading: true, + }; + if let Ok::, _>(data) = resp.try_into() { + if let Err(err) = clone_sink.send(data).await { + error!("Failed to send search result: {}", err); + break; } } } @@ -110,8 +108,8 @@ impl SearchManager { return; } - let resp = SearchResponsePB { - result: None, + let resp = SearchStatePB { + response: None, search_id: search_id.clone(), is_loading: true, }; From 99fb6ab743c3493a2621cf7605df237e86fbd75b Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 14 Apr 2025 23:07:42 +0800 Subject: [PATCH 311/384] chore: fix linux watch --- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 172204746a..f2c1d1d041 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,7 +5,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, @@ -58,7 +58,7 @@ pub struct LocalAIResourceController { user_service: Arc, resource_service: Arc, resource_notify: tokio::sync::broadcast::Sender<()>, - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "linux"))] #[allow(dead_code)] app_disk_watch: Option, app_state_sender: tokio::sync::broadcast::Sender, @@ -97,7 +97,7 @@ impl LocalAIResourceController { Self { user_service, resource_service: Arc::new(resource_service), - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "linux"))] app_disk_watch: offline_app_disk_watch, app_state_sender, resource_notify, From 1630e11c4d2a6260ce95758a1cce303ca878f863 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 15 Apr 2025 09:39:33 +0800 Subject: [PATCH 312/384] chore: update login page icons and i18n (#7742) * feat: update i18n and icons * chore: replace appflowy with welcome to appflowy * fix: protenial delete page error * fix: flutter analyze --- .../document_create_and_delete_test.dart | 2 + .../base/view_page/more_bottom_sheet.dart | 2 +- .../bottom_sheet/bottom_sheet_view_page.dart | 4 +- .../home/mobile_home_setting_page.dart | 8 +--- .../personal_info_setting_group.dart | 6 +-- .../lib/user/application/sign_in_bloc.dart | 7 +++ .../sign_in_screen/mobile_sign_in_screen.dart | 44 +++++-------------- .../widgets/anonymous_sign_in_button.dart | 2 +- .../continue_with_email_and_password.dart | 14 +++--- .../widgets/sign_in_agreement.dart | 5 +-- .../widgets/sign_in_anonymous_button.dart | 4 +- .../resources/flowy_icons/20x/cloud_mode.svg | 3 ++ .../flowy_icons/20x/sign_in_settings.svg | 4 ++ frontend/resources/translations/en.json | 3 +- 14 files changed, 48 insertions(+), 60 deletions(-) create mode 100644 frontend/resources/flowy_icons/20x/cloud_mode.svg create mode 100644 frontend/resources/flowy_icons/20x/sign_in_settings.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart index 43320509ce..c2e00a4b48 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart @@ -13,6 +13,8 @@ void main() { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); + final finder = find.text(gettingStarted, findRichText: true); + await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2)); // create a new document const pageName = 'Test Document'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index 6609b10136..be134e0a92 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -66,7 +66,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { break; case MobileViewBottomSheetBodyAction.delete: context.read().add(const ViewEvent.delete()); - context.pop(); + Navigator.of(context).pop(); break; case MobileViewBottomSheetBodyAction.addToFavorites: _addFavorite(context); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 991cf82b5d..9497774298 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -182,7 +182,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), _divider(), ..._buildPublishActions(context), - _divider(), + MobileQuickActionButton( text: LocaleKeys.button_delete.tr(), textColor: Theme.of(context).colorScheme.error, @@ -236,6 +236,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { MobileViewBottomSheetBodyAction.unpublish, ), ), + _divider(), ]; } else { return [ @@ -246,6 +247,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { MobileViewBottomSheetBodyAction.publish, ), ), + _divider(), ]; } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart index 09e22fc746..a01df20549 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart @@ -71,12 +71,6 @@ class _MobileHomeSettingPageState extends State { } Widget _buildSettingsWidget(UserProfilePB userProfile) { - // show the third-party sign in buttons if user logged in with local session and auth is enabled. - - final isLocalAuthEnabled = - userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled; - ''; - return BlocProvider( create: (context) => UserWorkspaceBloc(userProfile: userProfile) ..add(const UserWorkspaceEvent.initial()), @@ -105,7 +99,7 @@ class _MobileHomeSettingPageState extends State { const AboutSettingGroup(), UserSessionSettingGroup( userProfile: userProfile, - showThirdPartyLogin: isLocalAuthEnabled, + showThirdPartyLogin: false, ), const VSpace(20), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index cfdf3defb0..37191a2ae2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -7,10 +5,10 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../widgets/widgets.dart'; - import 'personal_info.dart'; class PersonalInfoSettingGroup extends StatelessWidget { @@ -32,7 +30,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { selector: (state) => state.userProfile.name, builder: (context, userName) { return MobileSettingGroup( - groupTitle: LocaleKeys.settings_mobile_personalInfo.tr(), + groupTitle: LocaleKeys.settings_accountPage_title.tr(), settingItemList: [ MobileSettingItem( name: userName, diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 06a03765c9..339af51f9f 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -219,6 +219,13 @@ class SignInBloc extends Bloc { required String email, required String passcode, }) async { + if (state.isSubmitting) { + Log.error('Sign in with passcode is already in progress'); + return; + } + + Log.info('Sign in with passcode: $email, $passcode'); + emit( state.copyWith( isSubmitting: true, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 7e2222abf2..9eb7d5a965 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -7,6 +7,7 @@ import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/user/presentation/widgets/flowy_logo_title.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -21,34 +22,29 @@ class MobileSignInScreen extends StatelessWidget { @override Widget build(BuildContext context) { - const double spacing = 16; - final colorScheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { + final theme = AppFlowyTheme.of(context); return Scaffold( resizeToAvoidBottomInset: false, body: Padding( padding: const EdgeInsets.symmetric(vertical: 38, horizontal: 40), child: Column( children: [ - const Spacer(flex: 4), - _buildLogo(), - const VSpace(spacing), - _buildAppNameText(colorScheme), - const VSpace(spacing * 2), + const Spacer(), + FlowyLogoTitle(title: LocaleKeys.welcomeText.tr()), + VSpace(theme.spacing.xxl), isLocalAuthEnabled ? const SignInAnonymousButtonV3() : const ContinueWithEmailAndPassword(), - const VSpace(spacing), - if (isAuthEnabled) _buildThirdPartySignInButtons(context), - const VSpace(spacing * 1.5), + VSpace(theme.spacing.xxl), + if (isAuthEnabled) ...[ + _buildThirdPartySignInButtons(context), + VSpace(theme.spacing.xxl), + ], const SignInAgreement(), - const VSpace(spacing), - if (!isAuthEnabled) const Spacer(flex: 2), - const Spacer(flex: 2), const Spacer(), - Expanded(child: _buildSettingsButton(context)), - if (Platform.isAndroid) const Spacer(), + _buildSettingsButton(context), ], ), ), @@ -57,24 +53,6 @@ class MobileSignInScreen extends StatelessWidget { ); } - Widget _buildLogo() { - return const FlowySvg( - FlowySvgs.flowy_logo_xl, - size: Size.square(56), - blendMode: null, - ); - } - - Widget _buildAppNameText(ColorScheme colorScheme) { - return FlowyText( - LocaleKeys.appName.tr(), - textAlign: TextAlign.center, - fontSize: 28, - color: const Color(0xFF00BCF0), - fontWeight: FontWeight.w700, - ); - } - Widget _buildThirdPartySignInButtons(BuildContext context) { final theme = AppFlowyTheme.of(context); return Column( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart index 2b11068abb..a7a1b9722d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart @@ -29,7 +29,7 @@ class SignInAnonymousButtonV3 extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - final text = LocaleKeys.signUp_getStartedText.tr(); + final text = LocaleKeys.signIn_continueWithLocalModel.tr(); final onTap = state.anonUsers.isEmpty ? () { context diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index efa72e7341..8034dccd32 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -131,12 +131,14 @@ class _ContinueWithEmailAndPasswordState _hasPushedContinueWithMagicLinkOrPasscodePage = false; }, - onEnterPasscode: (passcode) => signInBloc.add( - SignInEvent.signInWithPasscode( - email: email, - passcode: passcode, - ), - ), + onEnterPasscode: (passcode) { + signInBloc.add( + SignInEvent.signInWithPasscode( + email: email, + passcode: passcode, + ), + ); + }, ), ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index 19a5ab9cf6..76ce87ffc1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -1,5 +1,4 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -25,9 +24,7 @@ class SignInAgreement extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: isLocalAuthEnabled - ? LocaleKeys.web_signInLocalAgreement.tr() - : LocaleKeys.web_signInAgreement.tr(), + text: LocaleKeys.web_signInAgreement.tr(), style: textStyle, ), TextSpan( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index 2b40502b0d..33ef1d7bb0 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -79,7 +79,7 @@ class ChangeCloudModeButton extends StatelessWidget { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); return AFGhostIconTextButton( - text: 'Cloud', + text: LocaleKeys.signIn_switchToAppFlowyCloud.tr(), textColor: (context, isHovering, disabled) { return theme.textColorScheme.secondary; }, @@ -97,7 +97,7 @@ class ChangeCloudModeButton extends StatelessWidget { }, iconBuilder: (context, isHovering, disabled) { return FlowySvg( - FlowySvgs.settings_s, + FlowySvgs.cloud_mode_m, size: Size.square(20), color: theme.textColorScheme.secondary, ); diff --git a/frontend/resources/flowy_icons/20x/cloud_mode.svg b/frontend/resources/flowy_icons/20x/cloud_mode.svg new file mode 100644 index 0000000000..5aaf68e3db --- /dev/null +++ b/frontend/resources/flowy_icons/20x/cloud_mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/sign_in_settings.svg b/frontend/resources/flowy_icons/20x/sign_in_settings.svg new file mode 100644 index 0000000000..5d88d23086 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/sign_in_settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 46cab3030a..351dc54de4 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -36,7 +36,8 @@ "loginButtonText": "Login", "loginStartWithAnonymous": "Continue with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", - "anonymous": "Anonymous", + "continueWithLocalModel": "Continue with local model", + "switchToAppFlowyCloud": "AppFlowy Cloud", "anonymousMode": "Anonymous mode", "buttonText": "Sign In", "signingInText": "Signing in...", From d01909830d83dcd621bf817190d00b98262472d5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:14:20 +0800 Subject: [PATCH 313/384] fix: ai writer not working in database rows (#7746) * fix: ai writer not working in database rows * chore: dart analysis --- .../database/widgets/row/row_document.dart | 42 +++++++++++-------- .../database_document_page.dart | 7 +++- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart index 8e229f6b21..436dbd085d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; @@ -120,29 +121,34 @@ class _RowEditor extends StatelessWidget { return context; }, dispose: (_, editorContext) => editorContext.dispose(), - child: EditorDropHandler( + child: AiWriterScrollWrapper( viewId: view.id, editorState: editorState, - isLocalMode: context.read().isLocalMode, - dropManagerState: context.read(), - child: EditorTransactionService( + child: EditorDropHandler( viewId: view.id, editorState: editorState, - child: Provider( - create: (context) => - DatabasePluginWidgetBuilderSize(horizontalPadding: 0), - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: const EdgeInsets.only(left: 16, right: 54), + isLocalMode: context.read().isLocalMode, + dropManagerState: context.read(), + child: EditorTransactionService( + viewId: view.id, + editorState: editorState, + child: Provider( + create: (context) => DatabasePluginWidgetBuilderSize( + horizontalPadding: 0, + ), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.only(left: 16, right: 54), + ), + showParagraphPlaceholder: (editorState, _) => + editorState.document.isEmpty, + placeholderText: (_) => + LocaleKeys.cardDetails_notesPlaceholder.tr(), ), - showParagraphPlaceholder: (editorState, _) => - editorState.document.isEmpty, - placeholderText: (_) => - LocaleKeys.cardDetails_notesPlaceholder.tr(), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart index 186671b427..9568784464 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; @@ -104,7 +105,11 @@ class _DatabaseDocumentPageState extends State { return BlocListener( listener: _onNotificationAction, listenWhen: (_, curr) => curr.action != null, - child: _buildEditorPage(context, state), + child: AiWriterScrollWrapper( + viewId: widget.view.id, + editorState: editorState, + child: _buildEditorPage(context, state), + ), ); }, ), From d9748d5ef1d95542bc07f51d36db4d82005403d2 Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 15 Apr 2025 13:50:53 +0800 Subject: [PATCH 314/384] fix: block does not expand with grid size (#7745) * fix: block does not expand with grid size * fix: replace listenForSizeChanged with SizeChangedLayoutNotifier --- .../desktop/database/database_view_test.dart | 37 ++++++++++++++++++ .../database/tab_bar/tab_bar_view.dart | 6 ++- .../base/built_in_page_widget.dart | 7 +++- .../simple_column_block_component.dart | 7 +++- .../simple_column_block_width_resizer.dart | 9 +++-- .../simple_columns_block_component.dart | 38 ++++++++++++++----- 6 files changed, 87 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart index e35c9cc9d8..71656c1ea6 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart @@ -1,5 +1,10 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -73,5 +78,37 @@ void main() { await tester.pumpAndSettle(); }); + + testWidgets('insert grid in column', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// create page and show slash menu + await tester.createNewPageWithNameUnderParent(name: 'test page'); + await tester.editor.tapLineOfEditorAt(0); + await tester.editor.showSlashMenu(); + await tester.pumpAndSettle(); + + /// create a column + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_twoColumns.tr(), + ); + final actionList = find.byType(BlockActionList); + expect(actionList, findsNWidgets(2)); + final position = tester.getCenter(actionList.last); + + /// tap the second child of column + await tester.tapAt(position.copyWith(dx: position.dx + 50)); + + /// create a grid + await tester.editor.showSlashMenu(); + await tester.pumpAndSettle(); + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_grid.tr(), + ); + + final grid = find.byType(GridPageContent); + expect(grid, findsOneWidget); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 3554f9112e..7c2dc40869 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -362,9 +362,13 @@ const kDatabasePluginWidgetBuilderActionBuilder = 'action_builder'; const kDatabasePluginWidgetBuilderNode = 'node'; class DatabasePluginWidgetBuilderSize { - const DatabasePluginWidgetBuilderSize({required this.horizontalPadding}); + const DatabasePluginWidgetBuilderSize({ + required this.horizontalPadding, + this.verticalPadding = 16.0, + }); final double horizontalPadding; + final double verticalPadding; } class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index f96a06b21d..090ecdce78 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; @@ -6,6 +7,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class BuiltInPageWidget extends StatefulWidget { const BuiltInPageWidget({ @@ -77,6 +79,9 @@ class _BuiltInPageWidgetState extends State { } Widget _buildPage(BuildContext context, ViewPB view) { + final verticalPadding = + context.read()?.verticalPadding ?? + 0.0; return Focus( focusNode: focusNode, onFocusChange: (value) { @@ -85,7 +90,7 @@ class _BuiltInPageWidgetState extends State { } }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(vertical: verticalPadding), child: widget.builder(view), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart index 979dd92ce0..c426ad640f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; @@ -115,7 +116,11 @@ class SimpleColumnBlockComponentState extends State crossAxisAlignment: CrossAxisAlignment.start, children: node.children.map( (e) { - Widget child = IntrinsicHeight( + Widget child = Provider( + create: (_) => DatabasePluginWidgetBuilderSize( + verticalPadding: 0, + horizontalPadding: 0, + ), child: editorState.renderer.build(context, e), ); if (SimpleColumnsBlockConstants.enableDebugBorder) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart index f94056cd3a..69bec33c61 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart @@ -9,10 +9,12 @@ class SimpleColumnBlockWidthResizer extends StatefulWidget { super.key, required this.columnNode, required this.editorState, + this.height, }); final Node columnNode; final EditorState editorState; + final double? height; @override State createState() => @@ -53,15 +55,14 @@ class _SimpleColumnBlockWidthResizerState child: ValueListenableBuilder( valueListenable: isHovering, builder: (context, isHovering, child) { - if (isDraggingAppFlowyEditorBlock.value) { - return SizedBox.shrink(); - } + final hide = isDraggingAppFlowyEditorBlock.value || !isHovering; return MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: Container( width: 2, + height: widget.height ?? 20, margin: EdgeInsets.symmetric(horizontal: 2), - color: isHovering + color: !hide ? Theme.of(context).colorScheme.primary : Colors.transparent, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index 8408c0f775..58ecde5f2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -92,17 +92,18 @@ class ColumnsBlockComponentState extends State final ScrollController scrollController = ScrollController(); + final ValueNotifier heightValueNotifier = ValueNotifier(null); + @override void initState() { super.initState(); - _updateColumnsBlock(); } @override void dispose() { scrollController.dispose(); - + heightValueNotifier.dispose(); super.dispose(); } @@ -110,15 +111,13 @@ class ColumnsBlockComponentState extends State Widget build(BuildContext context) { Widget child = Row( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.start, children: _buildChildren(), ); child = Align( alignment: Alignment.topLeft, - child: IntrinsicHeight( - child: child, - ), + child: child, ); child = Padding( @@ -141,7 +140,10 @@ class ColumnsBlockComponentState extends State // the columns block does not support the block actions and selection // because the columns block is a layout wrapper, it does not have a content - return child; + return NotificationListener( + onNotification: (v) => updateHeightValueNotifier(v), + child: SizeChangedLayoutNotifier(child: child), + ); } List _buildChildren() { @@ -164,9 +166,15 @@ class ColumnsBlockComponentState extends State if (i != length - 1) { children.add( - SimpleColumnBlockWidthResizer( - columnNode: childNode, - editorState: editorState, + ValueListenableBuilder( + valueListenable: heightValueNotifier, + builder: (context, height, child) { + return SimpleColumnBlockWidthResizer( + columnNode: childNode, + editorState: editorState, + height: height, + ); + }, ), ); } @@ -194,6 +202,16 @@ class ColumnsBlockComponentState extends State } } + bool updateHeightValueNotifier(SizeChangedLayoutNotification notification) { + if (!mounted) return true; + final height = _renderBox?.size.height; + if (heightValueNotifier.value == height) return true; + WidgetsBinding.instance.addPostFrameCallback((_) { + heightValueNotifier.value = height; + }); + return true; + } + @override Position start() => Position(path: widget.node.path); From 3b3ae7fde93bb71ea959e18958b32a511e8eaef1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 15:59:15 +0800 Subject: [PATCH 315/384] chore: loading for search summary --- .../command_palette/command_palette_bloc.dart | 43 +++--- .../command_palette/search_service.dart | 14 +- .../command_palette/command_palette.dart | 4 +- .../widgets/search_result_cell.dart | 9 +- .../widgets/search_results_list.dart | 125 +++++++++++++++--- .../widgets/search_summary_cell.dart | 67 +++++++--- frontend/resources/translations/en.json | 2 + frontend/rust-lib/Cargo.lock | 42 ++---- frontend/rust-lib/Cargo.toml | 4 +- .../flowy-search/src/document/handler.rs | 17 ++- .../flowy-search/src/entities/notification.rs | 3 - .../flowy-search/src/entities/result.rs | 11 ++ .../flowy-search/src/services/manager.rs | 2 - 13 files changed, 250 insertions(+), 93 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index b53f3f8c23..01f638fe7a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -94,15 +94,16 @@ class CommandPaletteBloc emit( state.copyWith( query: null, - isLoading: false, + searching: false, serverResponseItems: [], localResponseItems: [], combinedResponseItems: {}, resultSummaries: [], + generatingAIOverview: false, ), ); } else { - emit(state.copyWith(query: event.search, isLoading: true)); + emit(state.copyWith(query: event.search, searching: true)); _activeQuery = event.search; unawaited( @@ -122,7 +123,8 @@ class CommandPaletteBloc add( CommandPaletteEvent.resultsChanged( searchId: '', - isLoading: false, + searching: false, + generatingAIOverview: false, ), ); } @@ -150,19 +152,23 @@ class CommandPaletteBloc searchId: searchId, localItems: items, ), - onServerItems: (items, searchId, isLoading) => _handleResultsUpdate( + onServerItems: (items, searchId, searching, generatingAIOverview) => + _handleResultsUpdate( searchId: searchId, serverItems: items, - isLoading: isLoading, + searching: searching, + generatingAIOverview: generatingAIOverview, ), - onSummaries: (summaries, searchId, isLoading) => _handleResultsUpdate( + onSummaries: (summaries, searchId, searching, generatingAIOverview) => + _handleResultsUpdate( searchId: searchId, summaries: summaries, - isLoading: isLoading, + searching: searching, + generatingAIOverview: generatingAIOverview, ), onFinished: (searchId) => _handleResultsUpdate( searchId: searchId, - isLoading: false, + searching: false, ), ); } @@ -172,7 +178,8 @@ class CommandPaletteBloc List? serverItems, List? localItems, List? summaries, - bool isLoading = true, + bool searching = true, + bool generatingAIOverview = false, }) { if (_isActiveSearch(searchId)) { add( @@ -181,7 +188,8 @@ class CommandPaletteBloc serverItems: serverItems, localItems: localItems, summaries: summaries, - isLoading: isLoading, + searching: searching, + generatingAIOverview: generatingAIOverview, ), ); } @@ -223,7 +231,8 @@ class CommandPaletteBloc localResponseItems: event.localItems ?? state.localResponseItems, resultSummaries: event.summaries ?? state.resultSummaries, combinedResponseItems: combinedItems, - isLoading: event.isLoading, + searching: event.searching, + generatingAIOverview: event.generatingAIOverview, ), ); } @@ -256,7 +265,8 @@ class CommandPaletteBloc localResponseItems: [], combinedResponseItems: {}, resultSummaries: [], - isLoading: false, + searching: false, + generatingAIOverview: false, ), ); } @@ -283,7 +293,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent { }) = _NewSearchStream; const factory CommandPaletteEvent.resultsChanged({ required String searchId, - required bool isLoading, + required bool searching, + required bool generatingAIOverview, List? serverItems, List? localItems, List? summaries, @@ -324,12 +335,14 @@ class CommandPaletteState with _$CommandPaletteState { @Default({}) Map combinedResponseItems, @Default([]) List resultSummaries, @Default(null) SearchResponseStream? searchResponseStream, - required bool isLoading, + required bool searching, + required bool generatingAIOverview, @Default([]) List trash, @Default(null) String? searchId, }) = _CommandPaletteState; factory CommandPaletteState.initial() => const CommandPaletteState( - isLoading: false, + searching: false, + generatingAIOverview: false, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart index 6f05b88081..89e5b604f8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -49,12 +49,14 @@ class SearchResponseStream { void Function( List items, String searchId, - bool isLoading, + bool searching, + bool generatingAIOverview, )? _onServerItems; void Function( List summaries, String searchId, - bool isLoading, + bool searching, + bool generatingAIOverview, )? _onSummaries; void Function( @@ -78,14 +80,16 @@ class SearchResponseStream { _onServerItems?.call( searchState.response.searchResult.items, searchId, - searchState.isLoading, + searchState.response.searching, + searchState.response.generatingAiSummary, ); } if (searchState.response.hasSearchSummary()) { _onSummaries?.call( searchState.response.searchSummary.items, searchId, - searchState.isLoading, + searchState.response.searching, + searchState.response.generatingAiSummary, ); } @@ -105,11 +109,13 @@ class SearchResponseStream { List items, String searchId, bool isLoading, + bool generatingAIOverview, )? onServerItems, required void Function( List summaries, String searchId, bool isLoading, + bool generatingAIOverview, )? onSummaries, required void Function( List items, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index a08c46679c..648712bd15 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -144,7 +144,7 @@ class CommandPaletteModal extends StatelessWidget { // Change mainAxisSize to max so Expanded works correctly. Column( children: [ - SearchField(query: state.query, isLoading: state.isLoading), + SearchField(query: state.query, isLoading: state.searching), if (state.query?.isEmpty ?? true) ...[ const Divider(height: 0), Flexible( @@ -167,7 +167,7 @@ class CommandPaletteModal extends StatelessWidget { // When there are no results and the query is not empty and not loading, // show the no results message, centered in the available space. else if ((state.query?.isNotEmpty ?? false) && - !state.isLoading) ...[ + !state.searching) ...[ const Divider(height: 0), Expanded( child: const _NoResultsHint(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart index 7034b79821..e8e5a37f82 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart @@ -207,9 +207,12 @@ class SearchResultPreview extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText(LocaleKeys.commandPalette_pagePreview.tr()), - const Divider( - thickness: 1, + Opacity( + opacity: 0.5, + child: FlowyText( + LocaleKeys.commandPalette_pagePreview.tr(), + fontSize: 12, + ), ), const VSpace(6), Expanded( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index af2944d09c..483da06f40 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -3,6 +3,7 @@ import 'package:appflowy/workspace/application/action_navigation/action_navigati import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -11,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'search_result_cell.dart'; import 'search_summary_cell.dart'; @@ -35,22 +37,37 @@ class SearchResultList extends StatelessWidget { ), ); - Widget _buildSummariesSection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: resultSummaries.length, - separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) => SearchSummaryCell( - summary: resultSummaries[index], + Widget _buildAIOverviewSection(BuildContext context) { + final state = context.read().state; + + if (state.generatingAIOverview) { + return Row( + children: [ + _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), + const HSpace(10), + const AIOverviewIndicator(), + ], + ); + } + if (resultSummaries.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), + ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: resultSummaries.length, + separatorBuilder: (_, __) => const Divider(height: 0), + itemBuilder: (_, index) => SearchSummaryCell( + summary: resultSummaries[index], + ), ), - ), - ], - ); + ], + ); + } + + return const SizedBox.shrink(); } Widget _buildResultsSection(BuildContext context) { @@ -83,6 +100,8 @@ class SearchResultList extends StatelessWidget { child: BlocProvider( create: (context) => SearchResultListBloc(), child: BlocListener( + listenWhen: (previous, current) => + previous.openPageId != current.openPageId, listener: (context, state) { if (state.openPageId != null) { FlowyOverlay.pop(context); @@ -102,24 +121,28 @@ class SearchResultList extends StatelessWidget { shrinkWrap: true, physics: const ClampingScrollPhysics(), children: [ - if (resultSummaries.isNotEmpty) _buildSummariesSection(), + _buildAIOverviewSection(context), const VSpace(10), if (resultItems.isNotEmpty) _buildResultsSection(context), ], ), ), const HSpace(10), - if (resultItems.any((item) => item.content.isNotEmpty)) + if (resultItems.any((item) => item.content.isNotEmpty)) ...[ + const VerticalDivider( + thickness: 1.0, + ), Flexible( flex: 3, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 12, + horizontal: 8, vertical: 16, ), child: const SearchCellPreview(), ), ), + ], ], ), ), @@ -145,3 +168,69 @@ class SearchCellPreview extends StatelessWidget { ); } } + +class AIOverviewIndicator extends StatelessWidget { + const AIOverviewIndicator({ + super.key, + this.duration = const Duration(seconds: 1), + }); + + final Duration duration; + + @override + Widget build(BuildContext context) { + final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); + return SelectionContainer.disabled( + child: SizedBox( + height: 20, + width: 100, + child: SeparatedRow( + separatorBuilder: () => const HSpace(4), + children: [ + buildDot(const Color(0xFF9327FF)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(duration: slice * 2, begin: 0, end: 0), + buildDot(const Color(0xFFFB006D)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: 0) + .then() + .slideY(begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(begin: 0, end: 0), + buildDot(const Color(0xFFFFCE00)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice * 2, begin: 0, end: 0) + .then() + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0), + ], + ), + ), + ); + } + + Widget buildDot(Color color) { + return SizedBox.square( + dimension: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart index 15e8bb18f7..89553709fc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -56,17 +57,50 @@ class SearchSummaryPreview extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText(LocaleKeys.commandPalette_aiOverviewSource.tr()), - const Divider( - thickness: 1, + if (summary.highlights.isNotEmpty) ...[ + Opacity( + opacity: 0.5, + child: FlowyText( + LocaleKeys.commandPalette_aiOverviewHighlights.tr(), + fontSize: 12, + ), + ), + const VSpace(6), + SearchSummaryHighlight(text: summary.highlights), + const VSpace(36), + ], + + Opacity( + opacity: 0.5, + child: FlowyText( + LocaleKeys.commandPalette_aiOverviewSource.tr(), + fontSize: 12, + ), ), + // Sources const VSpace(6), ...summary.sources.map((e) => SearchSummarySource(source: e)), + + // Highlights ], ); } } +class SearchSummaryHighlight extends StatelessWidget { + const SearchSummaryHighlight({ + required this.text, + super.key, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return AIMarkdownText(markdown: text); + } +} + class SearchSummarySource extends StatelessWidget { const SearchSummarySource({ required this.source, @@ -78,18 +112,21 @@ class SearchSummarySource extends StatelessWidget { @override Widget build(BuildContext context) { final icon = source.icon.getIcon(); - return SizedBox( - height: 30, - child: FlowyButton( - leftIcon: icon, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - text: FlowyText(source.displayName), - onTap: () { - context.read().add( - SearchResultListEvent.openPage(pageId: source.id), - ); - }, + return FlowyTooltip( + message: LocaleKeys.commandPalette_clickToOpenPage.tr(), + child: SizedBox( + height: 30, + child: FlowyButton( + leftIcon: icon, + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + text: FlowyText(source.displayName), + onTap: () { + context.read().add( + SearchResultListEvent.openPage(pageId: source.id), + ); + }, + ), ), ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b176a08f9f..f9ddbdf012 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2699,7 +2699,9 @@ "bestMatches": "Best matches", "aiOverview": "AI overview", "aiOverviewSource": "Reference sources", + "aiOverviewHighlights": "Highlights", "pagePreview": "Content preview", + "clickToOpenPage": "Click to open page", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 671d8bccf2..44d659ed56 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "async-trait", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "bincode", "bytes", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "app-error", "jsonwebtoken", @@ -4098,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "bytes", @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,7 +5209,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5277,19 +5276,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6784,7 +6770,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8d8e11f6a0..4166331fd9 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,8 +105,8 @@ tantivy = { version = "0.24.0" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index fc68f850e5..d236f4b639 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -77,6 +77,12 @@ impl SearchHandler for DocumentSearchHandler { }; // Execute document search. + yield Ok( + CreateSearchResultPBArgs::default().searching(true) + .build() + .unwrap(), + ); + let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await { Ok(items) => items, Err(e) => { @@ -114,7 +120,9 @@ impl SearchHandler for DocumentSearchHandler { let search_result = RepeatedSearchResponseItemPB { items }; yield Ok( CreateSearchResultPBArgs::default() + .searching(false) .search_result(Some(search_result)) + .generating_ai_summary(true) .build() .unwrap(), ); @@ -138,7 +146,7 @@ impl SearchHandler for DocumentSearchHandler { }) .collect(); - SearchSummaryPB { content: v.content, sources } + SearchSummaryPB { content: v.content, sources, highlights: v.highlights } }) .collect(); @@ -146,12 +154,19 @@ impl SearchHandler for DocumentSearchHandler { yield Ok( CreateSearchResultPBArgs::default() .search_summary(Some(summary_result)) + .generating_ai_summary(false) .build() .unwrap(), ); } Err(e) => { warn!("Failed to generate search summary: {:?}", e); + yield Ok( + CreateSearchResultPBArgs::default() + .generating_ai_summary(false) + .build() + .unwrap(), + ); } } }) diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index 3f1cdab67a..4f12305d9a 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -8,9 +8,6 @@ pub struct SearchStatePB { #[pb(index = 2)] pub search_id: String, - - #[pb(index = 3)] - pub is_loading: bool, } #[derive(ProtoBuf_Enum, Debug, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index 8f5ba11ded..a01f01b074 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -18,6 +18,14 @@ pub struct SearchResponsePB { #[pb(index = 3, one_of)] #[builder(default)] pub local_search_result: Option, + + #[pb(index = 4)] + #[builder(default)] + pub searching: bool, + + #[pb(index = 5)] + #[builder(default)] + pub generating_ai_summary: bool, } #[derive(ProtoBuf, Default, Debug, Clone)] @@ -33,6 +41,9 @@ pub struct SearchSummaryPB { #[pb(index = 2)] pub sources: Vec, + + #[pb(index = 3)] + pub highlights: String, } #[derive(ProtoBuf, Default, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index 72a3c12793..a71449d5d2 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -93,7 +93,6 @@ impl SearchManager { let resp = SearchStatePB { response: Some(search_result), search_id: search_id.clone(), - is_loading: true, }; if let Ok::, _>(data) = resp.try_into() { if let Err(err) = clone_sink.send(data).await { @@ -111,7 +110,6 @@ impl SearchManager { let resp = SearchStatePB { response: None, search_id: search_id.clone(), - is_loading: true, }; if let Ok::, _>(data) = resp.try_into() { let _ = clone_sink.send(data).await; From 92912367338490e5c0a9294145786431af34a59e Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 16:49:33 +0800 Subject: [PATCH 316/384] chore: clippy --- .../widgets/search_results_list.dart | 1 - frontend/rust-lib/Cargo.lock | 42 ++++++++++++------- frontend/rust-lib/Cargo.toml | 4 +- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index 483da06f40..1a7a1d94bb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -3,7 +3,6 @@ import 'package:appflowy/workspace/application/action_navigation/action_navigati import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 44d659ed56..a84de4cdd6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "async-trait", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "bincode", "bytes", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "app-error", "jsonwebtoken", @@ -4098,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "bytes", @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,6 +5209,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5276,6 +5277,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6770,7 +6784,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a11b94240946fa8f0549e5cf1c6505b7fa7e0a16#a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9ea3c46c26b006decfd3c98fffe910dd49a6607d#9ea3c46c26b006decfd3c98fffe910dd49a6607d" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 4166331fd9..88972e5bfb 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,8 +105,8 @@ tantivy = { version = "0.24.0" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a11b94240946fa8f0549e5cf1c6505b7fa7e0a16" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9ea3c46c26b006decfd3c98fffe910dd49a6607d" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9ea3c46c26b006decfd3c98fffe910dd49a6607d" } [profile.dev] opt-level = 0 From 69b5452af5ad24c50c26e753b1ebed76e4c8804e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:17:39 +0800 Subject: [PATCH 317/384] fix: undo not working in database row full page (#7749) --- .../database_document_page.dart | 61 ++++++++----------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart index 9568784464..ee52be8c26 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; @@ -7,9 +8,10 @@ import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; @@ -19,8 +21,10 @@ import 'package:appflowy/workspace/application/action_navigation/navigation_acti import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; import '../../workspace/application/view/view_bloc.dart'; @@ -49,18 +53,6 @@ class DatabaseDocumentPage extends StatefulWidget { class _DatabaseDocumentPageState extends State { EditorState? editorState; - @override - void initState() { - super.initState(); - EditorNotification.addListener(_onEditorNotification); - } - - @override - void dispose() { - EditorNotification.removeListener(_onEditorNotification); - super.dispose(); - } - @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -126,21 +118,34 @@ class _DatabaseDocumentPageState extends State { styleCustomizer: EditorStyleCustomizer( context: context, padding: EditorStyleCustomizer.documentPadding, + editorState: state.editorState!, ), header: _buildDatabaseDataContent(context, state.editorState!), initialSelection: widget.initialSelection, useViewInfoBloc: false, + placeholderText: (node) => + node.type == ParagraphBlockKeys.type && !node.isInTable + ? LocaleKeys.editor_slashPlaceHolder.tr() + : '', ), ); - return EditorTransactionService( - viewId: widget.view.id, - editorState: state.editorState!, - child: Column( - children: [ - if (state.isDeleted) _buildBanner(context), - Expanded(child: appflowyEditorPage), - ], + return Provider( + create: (_) { + final context = SharedEditorContext(); + context.isInDatabaseRowPage = true; + return context; + }, + dispose: (_, editorContext) => editorContext.dispose(), + child: EditorTransactionService( + viewId: widget.view.id, + editorState: state.editorState!, + child: Column( + children: [ + if (state.isDeleted) _buildBanner(context), + Expanded(child: appflowyEditorPage), + ], + ), ), ); } @@ -213,20 +218,6 @@ class _DatabaseDocumentPageState extends State { ); } - void _onEditorNotification(EditorNotificationType type) { - final editorState = this.editorState; - if (editorState == null) { - return; - } - if (type == EditorNotificationType.undo) { - undoCommand.execute(editorState); - } else if (type == EditorNotificationType.redo) { - redoCommand.execute(editorState); - } else if (type == EditorNotificationType.exitEditing) { - editorState.selection = null; - } - } - void _onNotificationAction( BuildContext context, ActionNavigationState state, From c6511cfb55d4006169e737e38310ae3b76826f15 Mon Sep 17 00:00:00 2001 From: Morn Date: Tue, 15 Apr 2025 17:43:50 +0800 Subject: [PATCH 318/384] fix: mobile slash menu position error (#7747) * fix: mobile slash menu position error * fix: mobile slash menu sometimes not showing up --- .../selection_menu/mobile_selection_menu.dart | 77 +++++++++++-------- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart index 7f293cc1c9..0b700b6504 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -166,7 +165,8 @@ class MobileSelectionMenu extends SelectionMenuService { if (selectionRects.isEmpty) { return null; } - calculateSelectionMenuOffset(selectionRects.first); + final screenSize = MediaQuery.of(context).size; + calculateSelectionMenuOffset(selectionRects.first, screenSize); final (left, top, right, bottom) = getPosition(); return _Position(left, top, right, bottom); } @@ -205,50 +205,65 @@ class MobileSelectionMenu extends SelectionMenuService { return (left, top, right, bottom); } - void calculateSelectionMenuOffset(Rect rect) { + void calculateSelectionMenuOffset(Rect rect, Size screenSize) { // Workaround: We can customize the padding through the [EditorStyle], // but the coordinates of overlay are not properly converted currently. // Just subtract the padding here as a result. - const menuHeight = 192.0, menuWidth = 240.0 + 10; - const menuOffset = Offset(0, 10); + const menuHeight = 192.0, menuWidth = 240.0; final editorOffset = editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; final editorHeight = editorState.renderBox!.size.height; + final screenHeight = screenSize.height; final editorWidth = editorState.renderBox!.size.width; + final rectHeight = rect.height; // show below default - _alignment = Alignment.topLeft; - final bottomRight = rect.bottomRight; - final topRight = rect.topRight; - var offset = bottomRight + menuOffset; - final limitX = editorWidth - menuWidth + editorOffset.dx; + _alignment = Alignment.bottomRight; + final bottomRight = rect.topLeft; + final offset = bottomRight; + final limitX = editorWidth + editorOffset.dx - menuWidth, + limitY = screenHeight - + editorHeight + + editorOffset.dy - + menuHeight - + rectHeight; _offset = Offset( - min(offset.dx, limitX), - offset.dy, + editorWidth - offset.dx - menuWidth, + screenHeight - offset.dy - menuHeight - rectHeight, ); - // show above if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { - offset = topRight - menuOffset; - _alignment = Alignment.bottomLeft; - - _offset = Offset( - offset.dx, - editorOffset.dy + editorHeight - offset.dy, - ); + /// show above + if (offset.dy > menuHeight) { + _offset = Offset( + _offset.dx, + offset.dy - menuHeight, + ); + _alignment = Alignment.topRight; + } else { + _offset = Offset( + _offset.dx, + limitY, + ); + } } - // show on left - if (_offset.dx - editorOffset.dx > editorWidth / 2) { - _alignment = _alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - final x = editorWidth - _offset.dx + editorOffset.dx; - _offset = Offset( - min(x, limitX), - _offset.dy, - ); + if (offset.dx + menuWidth >= editorOffset.dx + editorWidth) { + /// show left + if (offset.dx > menuWidth) { + _alignment = _alignment == Alignment.bottomRight + ? Alignment.bottomLeft + : Alignment.topLeft; + _offset = Offset( + offset.dx - menuWidth, + _offset.dy, + ); + } else { + _offset = Offset( + limitX, + _offset.dy, + ); + } } } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1c6d66431d..cc574f7e64 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "361b99c38370abeeb19656f89e8c31cb3666623b" - resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b" + ref: "4967ed5" + resolved-ref: "4967ed57d9190948c08f868972c0babfdc470ba7" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0cc553d1a7..e9dfbf48d0 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -185,7 +185,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: '361b99c38370abeeb19656f89e8c31cb3666623b' + ref: '4967ed5' appflowy_editor_plugins: git: From f62686fdeb1304e3eaabd829d95f77adbd7ad9d4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 21:05:56 +0800 Subject: [PATCH 319/384] chore: support local chat --- frontend/rust-lib/Cargo.lock | 42 +++++++------------ frontend/rust-lib/Cargo.toml | 4 +- .../src/persistence/chat_message_sql.rs | 5 ++- frontend/rust-lib/flowy-server/src/lib.rs | 1 - .../impls/chat.rs} | 39 +++++++++++------ .../src/local_server/impls/mod.rs | 2 + .../flowy-server/src/local_server/server.rs | 13 ++++-- frontend/rust-lib/flowy-server/src/server.rs | 5 +-- 8 files changed, 59 insertions(+), 52 deletions(-) rename frontend/rust-lib/flowy-server/src/{default_impl.rs => local_server/impls/chat.rs} (83%) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 671d8bccf2..342e454b9d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "async-trait", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "bincode", "bytes", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "app-error", "jsonwebtoken", @@ -4098,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bytes", @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,7 +5209,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5277,19 +5276,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6784,7 +6770,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8d8e11f6a0..f1d811bf2d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,8 +105,8 @@ tantivy = { version = "0.24.0" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "39057f85a408a40dc702a9e54fd1b9dc91bb36e4" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "39057f85a408a40dc702a9e54fd1b9dc91bb36e4" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs index aa4dd8215d..6eaf6798e3 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs @@ -65,7 +65,10 @@ pub fn select_chat_messages( query = query.filter(chat_message_table::message_id.lt(before_message_id)); } query = query - .order((chat_message_table::message_id.desc(),)) + .order(( + chat_message_table::created_at.desc(), + chat_message_table::message_id.desc(), + )) .limit(limit_val); let messages: Vec = query.load::(&mut *conn)?; diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 33f4b0c0d8..034991a984 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -5,5 +5,4 @@ pub mod local_server; mod response; mod server; -mod default_impl; pub mod util; diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs similarity index 83% rename from frontend/rust-lib/flowy-server/src/default_impl.rs rename to frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 02e313115f..a68dbc8b4f 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -6,15 +6,16 @@ use flowy_ai_pub::cloud::{ }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use lib_infra::util::timestamp; use serde_json::Value; use std::collections::HashMap; use std::path::Path; use uuid::Uuid; -pub(crate) struct DefaultChatCloudServiceImpl; +pub(crate) struct LocalServerChatServiceImpl; #[async_trait] -impl ChatCloudService for DefaultChatCloudServiceImpl { +impl ChatCloudService for LocalServerChatServiceImpl { async fn create_chat( &self, _uid: &i64, @@ -22,29 +23,40 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { _chat_id: &Uuid, _rag_ids: Vec, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(()) } async fn create_question( &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message: &str, - _message_type: ChatMessageType, + message: &str, + message_type: ChatMessageType, _metadata: &[ChatMessageMetadata], ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + match message_type { + ChatMessageType::System => Ok(ChatMessage::new_system(timestamp(), message.to_string())), + ChatMessageType::User => Ok(ChatMessage::new_human( + timestamp(), + message.to_string(), + None, + )), + } } async fn create_answer( &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message: &str, - _question_id: i64, - _metadata: Option, + message: &str, + question_id: i64, + metadata: Option, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let mut message = ChatMessage::new_ai(timestamp(), message.to_string(), Some(question_id)); + if let Some(metadata) = metadata { + message.metadata = metadata; + } + Ok(message) } async fn stream_answer( @@ -81,9 +93,12 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message_id: i64, + message_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], + }) } async fn get_answer( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs index 0280cfbefb..f63265e734 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs @@ -1,8 +1,10 @@ +pub(crate) use chat::*; pub(crate) use database::*; pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; +mod chat; mod database; mod document; mod folder; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index cb8b545c53..280a56cfe0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,13 +1,13 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use tokio::sync::mpsc; - +use flowy_ai_pub::cloud::ChatCloudService; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::FlowyError; use flowy_folder_pub::cloud::FolderCloudService; use flowy_storage_pub::cloud::StorageCloudService; +use tokio::sync::mpsc; // use flowy_user::services::database::{ // get_user_profile, get_user_workspace, open_collab_db, open_user_db, // }; @@ -15,8 +15,9 @@ use flowy_user_pub::cloud::UserCloudService; use flowy_user_pub::entities::*; use crate::local_server::impls::{ - LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, - LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl, + LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, + LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, + LocalServerUserAuthServiceImpl, }; use crate::AppFlowyServer; @@ -78,4 +79,8 @@ impl AppFlowyServer for LocalServer { fn database_ai_service(&self) -> Option> { None } + + fn chat_service(&self) -> Arc { + Arc::new(LocalServerChatServiceImpl) + } } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index ee07eefa5a..4c92fe28d2 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -12,7 +12,6 @@ use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage}; -use crate::default_impl::DefaultChatCloudServiceImpl; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; @@ -103,9 +102,7 @@ pub trait AppFlowyServer: Send + Sync + 'static { /// An `Arc` wrapping the `DocumentCloudService` interface. fn document_service(&self) -> Arc; - fn chat_service(&self) -> Arc { - Arc::new(DefaultChatCloudServiceImpl) - } + fn chat_service(&self) -> Arc; /// Bridge for the Cloud AI Search features /// From 846172a709856f6c91d30a2f8107af434eaa80b4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 22:19:16 +0800 Subject: [PATCH 320/384] chore: adjust ui --- .../search_result_list_bloc.dart | 5 ++ .../widgets/search_result_cell.dart | 14 +++- .../widgets/search_results_list.dart | 83 ++++++++++++++----- .../widgets/search_summary_cell.dart | 8 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 15 ++-- 5 files changed, 96 insertions(+), 29 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart index 58e5a951da..e5953ae61b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart @@ -25,6 +25,7 @@ class SearchResultListBloc state.copyWith( hoveredSummary: event.summary, hoveredResult: null, + userHovered: event.userHovered, openPageId: null, ), ); @@ -38,6 +39,7 @@ class SearchResultListBloc state.copyWith( hoveredSummary: null, hoveredResult: event.item, + userHovered: event.userHovered, openPageId: null, ), ); @@ -55,9 +57,11 @@ class SearchResultListBloc class SearchResultListEvent with _$SearchResultListEvent { const factory SearchResultListEvent.onHoverSummary({ required SearchSummaryPB summary, + required bool userHovered, }) = _OnHoverSummary; const factory SearchResultListEvent.onHoverResult({ required SearchResultItem item, + required bool userHovered, }) = _OnHoverResult; const factory SearchResultListEvent.openPage({ @@ -72,6 +76,7 @@ class SearchResultListState with _$SearchResultListState { @Default(null) SearchSummaryPB? hoveredSummary, @Default(null) SearchResultItem? hoveredResult, @Default(null) String? openPageId, + @Default(false) bool userHovered, }) = _SearchResultListState; factory SearchResultListState.initial() => const SearchResultListState(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart index e8e5a37f82..2485da4a69 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart @@ -17,10 +17,12 @@ class SearchResultCell extends StatefulWidget { super.key, required this.item, this.isTrashed = false, + this.isHovered = false, }); final SearchResultItem item; final bool isTrashed; + final bool isHovered; @override State createState() => _SearchResultCellState(); @@ -142,7 +144,10 @@ class _SearchResultCellState extends State { onFocusChange: (hasFocus) { setState(() { context.read().add( - SearchResultListEvent.onHoverResult(item: widget.item), + SearchResultListEvent.onHoverResult( + item: widget.item, + userHovered: true, + ), ); _hasFocus = hasFocus; }); @@ -150,10 +155,13 @@ class _SearchResultCellState extends State { child: FlowyHover( onHover: (value) { context.read().add( - SearchResultListEvent.onHoverResult(item: widget.item), + SearchResultListEvent.onHoverResult( + item: widget.item, + userHovered: true, + ), ); }, - isSelected: () => _hasFocus, + isSelected: () => _hasFocus || widget.isHovered, style: HoverStyle( borderRadius: BorderRadius.circular(8), hoverColor: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index 1a7a1d94bb..d90888e3e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -16,7 +16,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'search_result_cell.dart'; import 'search_summary_cell.dart'; -class SearchResultList extends StatelessWidget { +class SearchResultList extends StatefulWidget { const SearchResultList({ required this.trash, required this.resultItems, @@ -27,6 +27,26 @@ class SearchResultList extends StatelessWidget { final List trash; final List resultItems; final List resultSummaries; + + @override + State createState() => _SearchResultListState(); +} + +class _SearchResultListState extends State { + late final SearchResultListBloc bloc; + + @override + void initState() { + super.initState(); + bloc = SearchResultListBloc(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } + Widget _buildSectionHeader(String title) => Padding( padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8), @@ -48,7 +68,21 @@ class SearchResultList extends StatelessWidget { ], ); } - if (resultSummaries.isNotEmpty) { + + if (widget.resultSummaries.isNotEmpty) { + if (!bloc.state.userHovered) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + bloc.add( + SearchResultListEvent.onHoverSummary( + summary: widget.resultSummaries[0], + userHovered: false, + ), + ); + }, + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -56,10 +90,11 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultSummaries.length, + itemCount: widget.resultSummaries.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) => SearchSummaryCell( - summary: resultSummaries[index], + summary: widget.resultSummaries[index], + isHovered: bloc.state.hoveredSummary != null, ), ), ], @@ -78,13 +113,14 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultItems.length, + itemCount: widget.resultItems.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) { - final item = resultItems[index]; + final item = widget.resultItems[index]; return SearchResultCell( item: item, - isTrashed: trash.any((t) => t.id == item.id), + isTrashed: widget.trash.any((t) => t.id == item.id), + isHovered: bloc.state.hoveredResult?.id == item.id, ); }, ), @@ -96,11 +132,9 @@ class SearchResultList extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), - child: BlocProvider( - create: (context) => SearchResultListBloc(), + child: BlocProvider.value( + value: bloc, child: BlocListener( - listenWhen: (previous, current) => - previous.openPageId != current.openPageId, listener: (context, state) { if (state.openPageId != null) { FlowyOverlay.pop(context); @@ -116,18 +150,27 @@ class SearchResultList extends StatelessWidget { children: [ Flexible( flex: 7, - child: ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - _buildAIOverviewSection(context), - const VSpace(10), - if (resultItems.isNotEmpty) _buildResultsSection(context), - ], + child: BlocBuilder( + buildWhen: (previous, current) => + previous.hoveredResult != current.hoveredResult || + previous.hoveredSummary != current.hoveredSummary, + builder: (context, state) { + return ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + _buildAIOverviewSection(context), + const VSpace(10), + if (widget.resultItems.isNotEmpty) + _buildResultsSection(context), + ], + ); + }, ), ), const HSpace(10), - if (resultItems.any((item) => item.content.isNotEmpty)) ...[ + if (widget.resultItems + .any((item) => item.content.isNotEmpty)) ...[ const VerticalDivider( thickness: 1.0, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart index 89553709fc..20a02589f5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -14,17 +14,23 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class SearchSummaryCell extends StatelessWidget { const SearchSummaryCell({ required this.summary, + required this.isHovered, super.key, }); final SearchSummaryPB summary; + final bool isHovered; @override Widget build(BuildContext context) { return FlowyHover( + isSelected: () => isHovered, onHover: (value) { context.read().add( - SearchResultListEvent.onHoverSummary(summary: summary), + SearchResultListEvent.onHoverSummary( + summary: summary, + userHovered: true, + ), ); }, style: HoverStyle( diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 0b27f4f525..ec12ac4963 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -453,7 +453,6 @@ impl AIManager { if let Some(local_model) = self.local_ai.get_plugin_chat_model() { let model = AIModel::local(local_model, "".to_string()); current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); models.push(model); } @@ -466,7 +465,7 @@ impl AIManager { } // Global active model is the model selected by the user in the workspace settings. - let server_active_model = self + let mut server_active_model = self .get_workspace_select_model() .await .map(|m| AIModel::server(m, "".to_string())) @@ -478,10 +477,14 @@ impl AIManager { ); let mut user_selected_model = server_active_model.clone(); + // when current select model is deprecated, reset the model to default + if !models.iter().any(|m| m.name == server_active_model.name) { + server_active_model = AIModel::default(); + } + let source_key = ai_available_models_key(&source); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. + // We use source to identify user selected model. source can be document id or chat id. match self.store_preferences.get_object::(&source_key) { None => { // when there is selected model and current local ai is active, then use local ai @@ -491,6 +494,8 @@ impl AIManager { }, Some(mut model) => { trace!("[Model Selection] user previous select model: {:?}", model); + // If source is provided, try to get the user-selected model from the store. User selected + // model will be used as the active model if it exists. if model.is_local { if let Some(local_ai_model) = ¤t_active_local_ai_model { if local_ai_model.name != model.name { @@ -508,7 +513,7 @@ impl AIManager { .iter() .find(|m| m.name == user_selected_model.name) .cloned() - .or(Some(server_active_model)); + .or(Some(server_active_model.clone())); // Update the stored preference if a different model is used. if let Some(ref active_model) = active_model { From 5c3e81e6dc7febb02661b29b593532063e70636c Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 22:35:06 +0800 Subject: [PATCH 321/384] chore: adjust ui --- .../application/settings/ai/local_ai_bloc.dart | 17 ----------------- .../widgets/search_summary_cell.dart | 4 +--- .../pages/setting_ai_view/ollama_setting.dart | 1 + frontend/resources/translations/en.json | 2 +- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index c4a4ee3afa..a90f319a94 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -125,21 +125,4 @@ class LocalAiPluginState with _$LocalAiPluginState { orElse: () => false, ); } - - bool get showSettings { - return maybeWhen( - ready: (isEnabled, _, runningState, lackOfResource) { - final isConnecting = [ - RunningStatePB.Connecting, - RunningStatePB.Connected, - ].contains(runningState); - - final resourcesReadyOrMissingModel = lackOfResource == null || - lackOfResource.resourceType == LackOfAIResourceTypePB.MissingModel; - - return !isConnecting && resourcesReadyOrMissingModel; - }, - orElse: () => false, - ); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart index 20a02589f5..84b8f6646b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -67,7 +67,7 @@ class SearchSummaryPreview extends StatelessWidget { Opacity( opacity: 0.5, child: FlowyText( - LocaleKeys.commandPalette_aiOverviewHighlights.tr(), + LocaleKeys.commandPalette_aiOverviewMoreDetails.tr(), fontSize: 12, ), ), @@ -86,8 +86,6 @@ class SearchSummaryPreview extends StatelessWidget { // Sources const VSpace(6), ...summary.sources.map((e) => SearchSummarySource(source: e)), - - // Highlights ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart index 2abcb552d1..6f38043927 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart @@ -62,6 +62,7 @@ class _SettingItemWidget extends StatelessWidget { SizedBox( height: 32, child: FlowyTextField( + autoFocus: false, hintText: item.hintText, text: item.content, onChanged: (content) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f9ddbdf012..6274540914 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2699,7 +2699,7 @@ "bestMatches": "Best matches", "aiOverview": "AI overview", "aiOverviewSource": "Reference sources", - "aiOverviewHighlights": "Highlights", + "aiOverviewMoreDetails": "More details", "pagePreview": "Content preview", "clickToOpenPage": "Click to open page", "recentHistory": "Recent history", From 9b077969e785ea4ef42090b782b8ef6b27269c3f Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 16 Apr 2025 10:11:31 +0800 Subject: [PATCH 322/384] fix: error position for slash search result (#7758) --- .../selection_menu/mobile_selection_menu.dart | 2 + .../mobile_selection_menu_widget.dart | 52 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart index 0b700b6504..f69360575a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart @@ -70,6 +70,7 @@ class MobileSelectionMenu extends SelectionMenuService { final editorWidth = editorState.renderBox!.size.width; _positionNotifier = ValueNotifier(position); + final showAtTop = position.top != null; _selectionMenuEntry = OverlayEntry( builder: (context) { return SizedBox( @@ -93,6 +94,7 @@ class MobileSelectionMenu extends SelectionMenuService { child: MobileSelectionMenuWidget( selectionMenuStyle: style, singleColumn: singleColumn, + showAtTop: showAtTop, items: selectionMenuItems ..forEach((element) { if (element is MobileSelectionMenuItem) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart index e259d49d52..d96dd224e1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart @@ -22,6 +22,7 @@ class MobileSelectionMenuWidget extends StatefulWidget { required this.deleteSlashByDefault, required this.singleColumn, required this.startOffset, + required this.showAtTop, this.nameBuilder, }); @@ -38,6 +39,7 @@ class MobileSelectionMenuWidget extends StatefulWidget { final bool deleteSlashByDefault; final bool singleColumn; + final bool showAtTop; final int startOffset; final SelectionMenuItemNameBuilder? nameBuilder; @@ -172,27 +174,37 @@ class _MobileSelectionMenuWidgetState extends State { @override Widget build(BuildContext context) { - return Focus( - focusNode: _focusNode, - child: DecoratedBox( - decoration: BoxDecoration( - color: widget.selectionMenuStyle.selectionMenuBackgroundColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: _showingItems.isEmpty - ? _buildNoResultsWidget(context) - : _buildResultsWidget( - context, - _showingItems, - widget.itemCountFilter, + return SizedBox( + height: 192, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.showAtTop) Spacer(), + Focus( + focusNode: _focusNode, + child: DecoratedBox( + decoration: BoxDecoration( + color: widget.selectionMenuStyle.selectionMenuBackgroundColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withValues(alpha: 0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), ), + child: _showingItems.isEmpty + ? _buildNoResultsWidget(context) + : _buildResultsWidget( + context, + _showingItems, + widget.itemCountFilter, + ), + ), + ), + if (!widget.showAtTop) Spacer(), + ], ), ); } From a5eb2cdd9a0171ecbc442aef09a8cf8db469a214 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 16 Apr 2025 10:15:40 +0800 Subject: [PATCH 323/384] feat: improve white label scripts on Windows (#7755) * feat: improve white label scripts on Windows * feat: add font white label script * chore: integrate font white label script --- .../assets/fonts/FlowyIconData.ttf | Bin 1904 -> 0 bytes .../ai_chat/presentation/chat_avatar.dart | 2 +- .../presentation/chat_welcome_page.dart | 2 +- .../sign_in_screen/widgets/logo/logo.dart | 2 +- .../presentation/screens/splash_screen.dart | 2 +- .../mobile_workspace_start_screen.dart | 2 +- .../notification/notification_service.dart | 14 +- .../appearance/desktop_appearance.dart | 2 + .../menu/sidebar/header/sidebar_top_menu.dart | 4 +- frontend/appflowy_flutter/pubspec.yaml | 7 +- ...lowy_ai_chat_logo.svg => ai_chat_logo.svg} | 0 .../16x/{flowy_logo.svg => app_logo.svg} | 0 .../40x/{flowy_logo.svg => app_logo.svg} | 0 ...k_mode.svg => app_logo_with_text_dark.svg} | 0 ..._text.svg => app_logo_with_text_light.svg} | 0 .../scripts/white_label/code_white_label.sh | 72 +++++++ .../scripts/white_label/font_white_label.sh | 198 ++++++++++++++++++ .../scripts/white_label/icon_white_label.sh | 49 +++-- frontend/scripts/white_label/white_label.sh | 30 ++- 19 files changed, 352 insertions(+), 34 deletions(-) delete mode 100644 frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf rename frontend/resources/flowy_icons/16x/{flowy_ai_chat_logo.svg => ai_chat_logo.svg} (100%) rename frontend/resources/flowy_icons/16x/{flowy_logo.svg => app_logo.svg} (100%) rename frontend/resources/flowy_icons/40x/{flowy_logo.svg => app_logo.svg} (100%) rename frontend/resources/flowy_icons/40x/{flowy_logo_dark_mode.svg => app_logo_with_text_dark.svg} (100%) rename frontend/resources/flowy_icons/40x/{flowy_logo_text.svg => app_logo_with_text_light.svg} (100%) create mode 100644 frontend/scripts/white_label/code_white_label.sh create mode 100644 frontend/scripts/white_label/font_white_label.sh diff --git a/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf deleted file mode 100644 index 8f03a5c8f932e4e65caac7823e05f2c1a0eb7e8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1904 zcmd^9L2D#M6#i;@GBeI>Cg4VqMM5Lln8h$NnS(5A48de41O%649b_+|ccy20LQi+o zUD;&zu*pG0FoGxX;#s^3q6dF~_y;_QvMe6v;6V|O8oyWF(aDnhf~}^yzW06aRlWD9 zZYTg19K%9mbLZW+?2oQL1k_Ekci-8mH&^YgL)s^_+nu2keYpPHm$ZN9eAM?xz0OaK z9|4O#h& zifhzgqHPDx(0%{C9f$gub0rEB{l(SWe*ndK>RVv$*7oUMs++e|GdV2rkvW}sE4wo* zf1x6K0Y886cW-jF#!7N5Atx+{#964C6^1RNOzu=f4$;eKU=_KI*4uYc^&K5B_qmo5&RAyg38r}5!<=MulCJ78mKF2LVag(V!MM}A-No7%V`chr z&)UYPbdwx8O!uUNrgGVzl>tkf%t5s?zdK|lVu>p&C7;K<@yQE6R;R3e40+Q|vu9RH zvVYdc71MyltPSjnwA!@WbWXZQ;_Pn8ofjLqk9q5+*~4OCPQ9U;tE#RRUQthuN~&7< zr&3j=qv8wd>*M0=%*<@@xX6&RxX&f-`l}185;v!2RN?8kKaWZh#x3M1;CcL#MiaL< z%}#tz(@Jp;MeJnQB7ZN#dE$PCr}($f87`1N%5WL4;UvQqT*WtXoAS@`oD=+zVTEhF zRVklCRYe)L$Un+(p7^s2PvJ%NRfY@Xzt36)fa#u7}Ym9`rqJFRtIPn~mnO zeSc(!qy~ZG+s?lB!Z@+l?4G}`wHpsQVNmOY!zH^H2HN%gP^^|89*njqZ8+N5cKiFj z6QASG8ErRC24P??*Ba;eO*e33N4s4~A$ibmYTdJYaX7SF*;#fJhkI^E*F3GGTlM;6 z4p>Ks2qVN8u;(6oZDWz_4X#c8Z<<&px=#b9PBf~GfF~xjk8BZSPj?s zcc=!aLDnI^cJUBKbe~W7pVl(g0ngep-T^jvg67<&Z=bvR#4+!WL|x=}xzaY-B&<@- zwpmq;)m`qtN&mpaNCqytj7htY-~n@KlGUt4s)@Phkf>$${=Y7pofU15@f~{COn)`m qTd4E7tPf}3t^EHl8kWi_OXX4R#$mME4G)4{Z_sro%)|>1Oys}7A{Fld diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart index d5ecd09c38..59b7fbd39b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart @@ -27,7 +27,7 @@ class ChatAIAvatar extends StatelessWidget { child: const CircleAvatar( backgroundColor: Colors.transparent, child: FlowySvg( - FlowySvgs.flowy_logo_s, + FlowySvgs.app_logo_s, size: Size.square(16), blendMode: null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index d7a90bd18a..30dc918f70 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -46,7 +46,7 @@ class ChatWelcomePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const FlowySvg( - FlowySvgs.flowy_logo_xl, + FlowySvgs.app_logo_xl, size: Size.square(32), blendMode: null, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart index eb29807b5b..8e126db7ad 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart @@ -12,7 +12,7 @@ class AFLogo extends StatelessWidget { @override Widget build(BuildContext context) { return FlowySvg( - FlowySvgs.flowy_logo_xl, + FlowySvgs.app_logo_xl, blendMode: null, size: size, ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 146bf06df1..71345aa8dd 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -115,7 +115,7 @@ class Body extends StatelessWidget { return Container( alignment: Alignment.center, child: UniversalPlatform.isMobile - ? const FlowySvg(FlowySvgs.flowy_logo_xl, blendMode: null) + ? const FlowySvg(FlowySvgs.app_logo_xl, blendMode: null) : const _DesktopSplashBody(), ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index 59b61aa54b..a6124da60b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -57,7 +57,7 @@ class _MobileWorkspaceStartScreenState children: [ const Spacer(), const FlowySvg( - FlowySvgs.flowy_logo_xl, + FlowySvgs.app_logo_xl, size: Size.square(64), blendMode: null, ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart index 7a19e2a822..3f9657c5cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart @@ -1,8 +1,12 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:local_notifier/local_notifier.dart'; +/// The app name used in the local notification. +/// +/// DO NOT Use i18n here, because the i18n plugin is not ready +/// before the local notification is initialized. +const _localNotifierAppName = 'AppFlowy'; + /// Manages Local Notifications /// /// Currently supports: @@ -12,7 +16,11 @@ import 'package:local_notifier/local_notifier.dart'; /// class NotificationService { static Future initialize() async { - await localNotifier.setup(appName: LocaleKeys.appName.tr()); + await localNotifier.setup( + appName: _localNotifierAppName, + // Don't create a shortcut on Windows, because the setup.exe will create a shortcut + shortcutPolicy: ShortcutPolicy.requireNoCreate, + ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 2a707b6b2d..c1e539cf58 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -14,6 +14,8 @@ class DesktopAppearance extends BaseAppearance { ) { assert(codeFontFamily.isNotEmpty); + fontFamily = fontFamily.isEmpty ? defaultFontFamily : fontFamily; + final isLight = brightness == Brightness.light; final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 559c189925..67930c336a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -50,8 +50,8 @@ class SidebarTopMenu extends StatelessWidget { } final svgData = Theme.of(context).brightness == Brightness.dark - ? FlowySvgs.flowy_logo_dark_mode_xl - : FlowySvgs.flowy_logo_text_xl; + ? FlowySvgs.app_logo_with_text_dark_xl + : FlowySvgs.app_logo_with_text_light_xl; return Padding( padding: const EdgeInsets.only(top: 12.0, left: 8), diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e9dfbf48d0..8fe956a194 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -254,9 +254,6 @@ flutter: uses-material-design: true fonts: - - family: FlowyIconData - fonts: - - asset: assets/fonts/FlowyIconData.ttf - family: Poppins fonts: - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf @@ -282,6 +279,9 @@ flutter: - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf style: italic + # White-label font configuration will be added here + # BEGIN: WHITE_LABEL_FONT + # END: WHITE_LABEL_FONT # To add assets to your application, add an assets section, like this: assets: @@ -298,6 +298,7 @@ flutter: - assets/images/login/ - assets/translations/ - assets/icons/icons.json + - assets/fonts/ # The following assets will be excluded in release. # BEGIN: EXCLUDE_IN_RELEASE diff --git a/frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg b/frontend/resources/flowy_icons/16x/ai_chat_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg rename to frontend/resources/flowy_icons/16x/ai_chat_logo.svg diff --git a/frontend/resources/flowy_icons/16x/flowy_logo.svg b/frontend/resources/flowy_icons/16x/app_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/flowy_logo.svg rename to frontend/resources/flowy_icons/16x/app_logo.svg diff --git a/frontend/resources/flowy_icons/40x/flowy_logo.svg b/frontend/resources/flowy_icons/40x/app_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/flowy_logo.svg rename to frontend/resources/flowy_icons/40x/app_logo.svg diff --git a/frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg b/frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg rename to frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg diff --git a/frontend/resources/flowy_icons/40x/flowy_logo_text.svg b/frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/flowy_logo_text.svg rename to frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg diff --git a/frontend/scripts/white_label/code_white_label.sh b/frontend/scripts/white_label/code_white_label.sh new file mode 100644 index 0000000000..1123a394ee --- /dev/null +++ b/frontend/scripts/white_label/code_white_label.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --company-name Set the custom company name" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --company-name \"MyCompany Ltd.\"" +} + +CUSTOM_COMPANY_NAME="" +CODE_FILE="appflowy_flutter/lib/workspace/application/notification/notification_service.dart" + +while [[ $# -gt 0 ]]; do + case $1 in + --company-name) + CUSTOM_COMPANY_NAME="$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$CUSTOM_COMPANY_NAME" ]; then + echo "Error: Company name is required" + show_usage + exit 1 +fi + +if [ ! -f "$CODE_FILE" ]; then + echo "Error: Code file not found at $CODE_FILE" + exit 1 +fi + +echo "Replacing '_localNotifierAppName' value with '$CUSTOM_COMPANY_NAME' in code file..." + +if sed --version >/dev/null 2>&1; then + SED_INPLACE="-i" +else + SED_INPLACE="-i ''" +fi + +echo "Processing code file..." +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + # First, escape any special characters in the company name + ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') + # Replace the _localNotifierAppName value with the custom company name + sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$ESCAPED_COMPANY_NAME'/" "$CODE_FILE" + if [ $? -ne 0 ]; then + echo "Error: Failed to process $CODE_FILE with sed" + exit 1 + fi +else + # For Unix-like systems + sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$CUSTOM_COMPANY_NAME'/" "$CODE_FILE" + if [ $? -ne 0 ]; then + echo "Error: Failed to process $CODE_FILE with sed" + exit 1 + fi +fi + +echo "Replacement complete!" diff --git a/frontend/scripts/white_label/font_white_label.sh b/frontend/scripts/white_label/font_white_label.sh new file mode 100644 index 0000000000..412ee6b062 --- /dev/null +++ b/frontend/scripts/white_label/font_white_label.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +show_usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" + echo " --font-family Set the name of the font family" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --font-path \"/path/to/fonts\" --font-family \"CustomFont\"" +} + +FONT_PATH="" +FONT_FAMILY="" +TARGET_FONT_DIR="appflowy_flutter/assets/fonts/" +PUBSPEC_FILE="appflowy_flutter/pubspec.yaml" +BASE_APPEARANCE_FILE="appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart" + +while [[ $# -gt 0 ]]; do + case $1 in + --font-path) + FONT_PATH="$2" + shift 2 + ;; + --font-family) + FONT_FAMILY="$2" + shift 2 + ;; + --help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Validate required arguments +if [ -z "$FONT_PATH" ]; then + echo "Error: Font path is required" + show_usage + exit 1 +fi + +if [ -z "$FONT_FAMILY" ]; then + echo "Error: Font family name is required" + show_usage + exit 1 +fi + +# Check if source directory exists +if [ ! -d "$FONT_PATH" ]; then + echo "Error: Font directory not found at $FONT_PATH" + exit 1 +fi + +# Create target directory if it doesn't exist +mkdir -p "$TARGET_FONT_DIR" + +# Clean existing fonts in target directory +echo "Cleaning existing fonts in $TARGET_FONT_DIR..." +rm -rf "$TARGET_FONT_DIR"/* + +# Copy font files to target directory +echo "Copying font files from $FONT_PATH to $TARGET_FONT_DIR..." +found_fonts=false +for ext in ttf otf; do + if ls "$FONT_PATH"/*."$ext" >/dev/null 2>&1; then + cp "$FONT_PATH"/*."$ext" "$TARGET_FONT_DIR"/ 2>/dev/null && found_fonts=true + fi +done + +if [ "$found_fonts" = false ]; then + echo "Error: No font files (.ttf or .otf) found in source directory" + exit 1 +fi + +# Generate font configuration for pubspec.yaml +echo "Generating font configuration..." + +# Create temporary file for font configuration +TEMP_FILE=$(mktemp) + +{ + echo " # BEGIN: WHITE_LABEL_FONT" + echo " - family: $FONT_FAMILY" + echo " fonts:" + + # Generate entries for each font file + for font_file in "$TARGET_FONT_DIR"/*; do + filename=$(basename "$font_file") + echo " - asset: assets/fonts/$filename" + + # Try to detect font weight from filename + if [[ $filename =~ (Thin|ExtraLight|Light|Regular|Medium|SemiBold|Bold|ExtraBold|Black) ]]; then + case ${BASH_REMATCH[1]} in + "Thin") echo " weight: 100";; + "ExtraLight") echo " weight: 200";; + "Light") echo " weight: 300";; + "Regular") echo " weight: 400";; + "Medium") echo " weight: 500";; + "SemiBold") echo " weight: 600";; + "Bold") echo " weight: 700";; + "ExtraBold") echo " weight: 800";; + "Black") echo " weight: 900";; + esac + fi + + # Try to detect italic style from filename + if [[ $filename =~ Italic ]]; then + echo " style: italic" + fi + done + echo " # END: WHITE_LABEL_FONT" +} > "$TEMP_FILE" + +# Update pubspec.yaml +echo "Updating pubspec.yaml..." +if [ -f "$PUBSPEC_FILE" ]; then + # Create a backup of the original file + cp "$PUBSPEC_FILE" "${PUBSPEC_FILE}.bak" + + if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + # Windows-specific handling + # First, remove existing white label font configuration + awk '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/{ next } /# White-label font configuration will be added here/{ print; system("cat '"$TEMP_FILE"'"); next } 1' "$PUBSPEC_FILE" > "${PUBSPEC_FILE}.tmp" + + if [ $? -eq 0 ]; then + mv "${PUBSPEC_FILE}.tmp" "$PUBSPEC_FILE" + rm -f "${PUBSPEC_FILE}.bak" + else + echo "Error: Failed to update pubspec.yaml" + mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" + rm -f "${PUBSPEC_FILE}.tmp" + rm -f "$TEMP_FILE" + exit 1 + fi + else + # Unix-like systems handling + if sed --version >/dev/null 2>&1; then + SED_INPLACE="-i" + else + SED_INPLACE="-i ''" + fi + + # Remove existing white label font configuration + sed $SED_INPLACE '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/d' "$PUBSPEC_FILE" + + # Add new font configuration + sed $SED_INPLACE "/# White-label font configuration will be added here/r $TEMP_FILE" "$PUBSPEC_FILE" + + if [ $? -ne 0 ]; then + echo "Error: Failed to update pubspec.yaml" + mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" + rm -f "$TEMP_FILE" + exit 1 + fi + rm -f "${PUBSPEC_FILE}.bak" + fi +else + echo "Error: pubspec.yaml not found at $PUBSPEC_FILE" + rm -f "$TEMP_FILE" + exit 1 +fi + +# Update base_appearance.dart +echo "Updating base_appearance.dart..." +if [ -f "$BASE_APPEARANCE_FILE" ]; then + # Create a backup of the original file + cp "$BASE_APPEARANCE_FILE" "${BASE_APPEARANCE_FILE}.bak" + + if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + # Windows-specific handling + sed -i "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" + else + # Unix-like systems handling + sed -i '' "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" + fi + + if [ $? -ne 0 ]; then + echo "Error: Failed to update base_appearance.dart" + mv "${BASE_APPEARANCE_FILE}.bak" "$BASE_APPEARANCE_FILE" + exit 1 + fi + rm -f "${BASE_APPEARANCE_FILE}.bak" +else + echo "Error: base_appearance.dart not found at $BASE_APPEARANCE_FILE" + exit 1 +fi + +# Cleanup +rm -f "$TEMP_FILE" + +echo "Font white labeling completed successfully!" diff --git a/frontend/scripts/white_label/icon_white_label.sh b/frontend/scripts/white_label/icon_white_label.sh index eb45d0f02f..ca70bc1661 100644 --- a/frontend/scripts/white_label/icon_white_label.sh +++ b/frontend/scripts/white_label/icon_white_label.sh @@ -3,16 +3,16 @@ show_usage() { echo "Usage: $0 [options]" echo "Options:" - echo " --icon-path Set the path to the application icon (.svg file)" + echo " --icon-path Set the path to the folder containing application icons (.svg files)" echo " --help Show this help message" echo "" echo "Example:" - echo " $0 --icon-path \"/path/to/new/icon.svg\"" + echo " $0 --icon-path \"/path/to/icons_folder\"" } NEW_ICON_PATH="" ICON_DIR="resources/flowy_icons" -ICON_NAME_NEED_REPLACE=("flowy_logo.svg" "flowy_ai_chat_logo.svg" "flowy_logo_dark_mode.svg" "flowy_logo_text.svg") +ICON_NAME_NEED_REPLACE=("app_logo.svg" "ai_chat_logo.svg" "app_logo_with_text_light.svg" "app_logo_with_text_dark.svg") while [[ $# -gt 0 ]]; do case $1 in @@ -38,12 +38,17 @@ if [ -z "$NEW_ICON_PATH" ]; then exit 1 fi +if [ ! -d "$NEW_ICON_PATH" ]; then + echo "Error: New icon directory not found at $NEW_ICON_PATH" + exit 1 +fi + if [ ! -d "$ICON_DIR" ]; then echo "Error: Icon directory not found at $ICON_DIR" exit 1 fi -echo "Replacing icon..." +echo "Replacing icons..." echo "Processing icon files..." if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then @@ -52,13 +57,18 @@ if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then echo "Checking subdirectory: $(basename "$subdir")" for file in "${subdir}"*.svg; do if [ -f "$file" ] && [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - echo "Updating: $(basename "$subdir")/$(basename "$file")" - cp "$NEW_ICON_PATH" "$file" - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") in $(basename "$subdir") with new icon" + new_icon="${NEW_ICON_PATH}/$(basename "$file")" + if [ -f "$new_icon" ]; then + echo "Updating: $(basename "$subdir")/$(basename "$file")" + cp "$new_icon" "$file" + if [ $? -eq 0 ]; then + echo "Successfully replaced $(basename "$file") in $(basename "$subdir") with new icon" + else + echo "Error: Failed to replace $(basename "$file") in $(basename "$subdir")" + exit 1 + fi else - echo "Error: Failed to replace $(basename "$file") in $(basename "$subdir")" - exit 1 + echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" fi fi done @@ -67,15 +77,18 @@ if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then else for file in $(find "$ICON_DIR" -name "*.svg" -type f); do if [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - echo "Updating: $(basename "$file")" - - cp "$NEW_ICON_PATH" "$file" - - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") with new icon" + new_icon="${NEW_ICON_PATH}/$(basename "$file")" + if [ -f "$new_icon" ]; then + echo "Updating: $(basename "$file")" + cp "$new_icon" "$file" + if [ $? -eq 0 ]; then + echo "Successfully replaced $(basename "$file") with new icon" + else + echo "Error: Failed to replace $(basename "$file")" + exit 1 + fi else - echo "Error: Failed to replace $(basename "$file")" - exit 1 + echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" fi fi done diff --git a/frontend/scripts/white_label/white_label.sh b/frontend/scripts/white_label/white_label.sh index 2d6004cf9d..8ecd187210 100644 --- a/frontend/scripts/white_label/white_label.sh +++ b/frontend/scripts/white_label/white_label.sh @@ -7,6 +7,8 @@ COMPANY_NAME="AppFlowy Inc." COPYRIGHT="Copyright © 2025 AppFlowy Inc." ICON_PATH="" WINDOWS_ICON_PATH="" +FONT_PATH="" +FONT_FAMILY="" PLATFORMS=("windows" "linux" "macos" "ios" "android") show_usage() { @@ -18,6 +20,8 @@ show_usage() { echo " --copyright Set the copyright information" echo " --icon-path Set the path to the application icon (.svg)" echo " --windows-icon-path Set the path to the windows application icon (.ico)" + echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" + echo " --font-family Set the name of the font family" echo " --platforms Comma-separated list of platforms to white label (windows,linux,macos,ios,android)" echo " --help Show this help message" echo "" @@ -26,7 +30,8 @@ show_usage() { echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" echo " --platforms \"windows,linux,macos\" \\" echo " --windows-icon-path \"./assets/icons/mycompany.ico\" \\" - echo " --icon-path \"./assets/icons/mycompany.svg\"" + echo " --icon-path \"./assets/icons/\" \\" + echo " --font-path \"./assets/fonts/\" --font-family \"CustomFont\"" } while [[ $# -gt 0 ]]; do @@ -55,6 +60,14 @@ while [[ $# -gt 0 ]]; do WINDOWS_ICON_PATH="$2" shift 2 ;; + --font-path) + FONT_PATH="$2" + shift 2 + ;; + --font-family) + FONT_FAMILY="$2" + shift 2 + ;; --platforms) IFS=',' read -ra PLATFORMS <<< "$2" shift 2 @@ -77,8 +90,8 @@ if [ -z "$APP_NAME" ] || [ -z "$APP_IDENTIFIER" ] || [ -z "$COMPANY_NAME" ] || [ exit 1 fi -if [ ! -f "$ICON_PATH" ]; then - echo "Error: Icon file not found at $ICON_PATH" +if [ ! -d "$ICON_PATH" ]; then + echo "Error: Icon directory not found at $ICON_PATH" exit 1 fi @@ -111,6 +124,17 @@ bash "scripts/white_label/i18n_white_label.sh" --company-name "$COMPANY_NAME" echo -e "\033[32mRunning icon white label script...\033[0m" bash "scripts/white_label/icon_white_label.sh" --icon-path "$ICON_PATH" +echo -e "\033[32mRunning code white label script...\033[0m" +bash "scripts/white_label/code_white_label.sh" --company-name "$COMPANY_NAME" + +# Run font white label script if font parameters are provided +if [ ! -z "$FONT_PATH" ] && [ ! -z "$FONT_FAMILY" ]; then + echo -e "\033[32mRunning font white label script...\033[0m" + bash "scripts/white_label/font_white_label.sh" \ + --font-path "$FONT_PATH" \ + --font-family "$FONT_FAMILY" +fi + for platform in "${PLATFORMS[@]}"; do run_platform_script "$platform" done From be1d2b4b9259fb0a692f64c3b7fb14aa8aa4b2f1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 12:06:20 +0800 Subject: [PATCH 324/384] chore: delay collab initialization until schema is configured; finalize to ensure collab includes its schema --- frontend/rust-lib/collab-integrate/src/collab_builder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index e31c38d043..10149bf259 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -277,7 +277,7 @@ impl AppFlowyCollabBuilder { let collab_db = collab_db.clone(); let device_id = self.workspace_integrate.device_id()?; let collab = tokio::task::spawn_blocking(move || { - let mut collab = CollabBuilder::new(object.uid, &object.object_id, data_source) + let collab = CollabBuilder::new(object.uid, &object.object_id, data_source) .with_device_id(device_id) .build()?; let persistence_config = CollabPersistenceConfig::default(); @@ -290,7 +290,6 @@ impl AppFlowyCollabBuilder { persistence_config, ); collab.add_plugin(Box::new(db_plugin)); - collab.initialize(); Ok::<_, Error>(collab) }) .await??; From 3214ec075be8dd3b2d51a309c66f6aa6cfcd419d Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 14:04:12 +0800 Subject: [PATCH 325/384] chore: fix flaky initialize folder indexer --- .../tasks/app_window_size_manager.dart | 8 ++- .../rust-lib/flowy-folder/src/manager_init.rs | 7 ++- .../flowy-search/src/folder/indexer.rs | 51 +++++++++++++++---- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart index 7d41f2dceb..5636ed70cb 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart @@ -12,6 +12,10 @@ class WindowSizeManager { static const double maxWindowHeight = 8192.0; static const double maxWindowWidth = 8192.0; + // Default windows size + static const double defaultWindowHeight = 960.0; + static const double defaultWindowWidth = 1280.0; + static const double maxScaleFactor = 2.0; static const double minScaleFactor = 0.5; @@ -36,8 +40,8 @@ class WindowSizeManager { Future getSize() async { final defaultWindowSize = jsonEncode( { - WindowSizeManager.height: minWindowHeight, - WindowSizeManager.width: minWindowWidth, + WindowSizeManager.height: defaultWindowHeight, + WindowSizeManager.width: defaultWindowWidth, }, ); final windowSize = await getIt().get(KVKeys.windowSize); diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index 59eb3e21ee..62cce7c394 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -138,7 +138,12 @@ impl FolderManager { Arc::downgrade(&self.user), ); - self.folder_indexer.initialize().await; + let weak_folder_indexer = Arc::downgrade(&self.folder_indexer); + tokio::spawn(async move { + if let Some(folder_indexer) = weak_folder_indexer.upgrade() { + folder_indexer.initialize().await; + } + }); Ok(()) } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 641c1f5f96..71ac5d5e60 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -1,3 +1,5 @@ +use super::entities::FolderIndexData; +use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB}; use crate::folder::schema::{ FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, @@ -7,27 +9,32 @@ use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexCont use flowy_error::{FlowyError, FlowyResult}; use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; use flowy_user::services::authenticate_user::AuthenticateUser; +use lib_infra::async_trait::async_trait; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::{collections::HashMap, fs}; - -use super::entities::FolderIndexData; -use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB}; -use lib_infra::async_trait::async_trait; use tantivy::{ collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, - Index, IndexReader, IndexWriter, TantivyDocument, Term, + Index, IndexReader, IndexWriter, TantivyDocument, TantivyError, Term, }; use tokio::sync::RwLock; use tracing::{error, info}; use uuid::Uuid; pub struct TantivyState { + pub path: PathBuf, pub index: Index, pub folder_schema: FolderSchema, pub index_reader: IndexReader, pub index_writer: IndexWriter, } +impl Drop for TantivyState { + fn drop(&mut self) { + tracing::trace!("Dropping TantivyState at {:?}", self.path); + } +} + const FOLDER_INDEX_DIR: &str = "folder_index"; #[derive(Clone)] @@ -57,7 +64,19 @@ impl FolderIndexManagerImpl { } /// Initializes the state using the workspace directory. - async fn initialize_with_workspace(&self) -> FlowyResult<()> { + async fn initialize(&self) -> FlowyResult<()> { + if let Some(state) = self.state.write().await.take() { + info!("Re-initializing folder indexer"); + drop(state); + } + + // Since the directory lock may not be immediately released, + // a workaround is implemented by waiting for 3 seconds before proceeding further. This delay helps + // to avoid errors related to trying to open an index directory while an IndexWriter is still active. + // + // Also, we don't need to initialize the indexer immediately. + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + let auth_user = self .auth_user .upgrade() @@ -73,12 +92,26 @@ impl FolderIndexManagerImpl { info!("Folder indexer initialized at: {:?}", index_path); let folder_schema = FolderSchema::new(); - let dir = MmapDirectory::open(index_path)?; + let dir = MmapDirectory::open(index_path.clone())?; let index = Index::open_or_create(dir, folder_schema.schema.clone())?; let index_reader = index.reader()?; - let index_writer = index.writer(50_000_000)?; + + let index_writer = match index.writer::<_>(50_000_000) { + Ok(index_writer) => index_writer, + Err(err) => { + if let TantivyError::LockFailure(_, _) = err { + error!( + "Failed to acquire lock for index writer: {:?}, retry later", + err + ); + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + } + index.writer::<_>(50_000_000)? + }, + }; *self.state.write().await = Some(TantivyState { + path: index_path, index, folder_schema, index_reader, @@ -295,7 +328,7 @@ impl IndexManager for FolderIndexManagerImpl { #[async_trait] impl FolderIndexManager for FolderIndexManagerImpl { async fn initialize(&self) { - if let Err(e) = self.initialize_with_workspace().await { + if let Err(e) = self.initialize().await { error!("Failed to initialize FolderIndexManager: {:?}", e); } } From e6951012f0a9f27df394a324e72a196b8bb30e6c Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 14:15:38 +0800 Subject: [PATCH 326/384] chore: do not generate search summary if result is empty --- frontend/rust-lib/flowy-search/src/document/handler.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index d236f4b639..2127ef0d98 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -90,6 +90,7 @@ impl SearchHandler for DocumentSearchHandler { return; } }; + trace!("[Search] search result: {:?}", result_items); // Prepare input for search summary generation. let summary_input: Vec = result_items @@ -122,11 +123,15 @@ impl SearchHandler for DocumentSearchHandler { CreateSearchResultPBArgs::default() .searching(false) .search_result(Some(search_result)) - .generating_ai_summary(true) + .generating_ai_summary(!result_items.is_empty()) .build() .unwrap(), ); + if result_items.is_empty() { + return; + } + // Generate and yield search summary. match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await { Ok(summary_result) => { From be132d867a17e213d1e638918d48e4bdf92ab5f0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 14:31:10 +0800 Subject: [PATCH 327/384] chore: lock analyzer version --- frontend/appflowy_flutter/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8fe956a194..28c9f32134 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -11,6 +11,7 @@ environment: sdk: '>=3.3.0 <4.0.0' dependencies: + analyzer: 6.11.0 any_date: ^1.0.4 app_links: ^6.3.3 appflowy_backend: From ecfcf3be4d15d82fcabb50905be3c38ec71fd617 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 14:37:45 +0800 Subject: [PATCH 328/384] chore: commit pub lock --- frontend/appflowy_flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index cc574f7e64..120a630a8c 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -15,7 +15,7 @@ packages: source: sdk version: "0.3.3" analyzer: - dependency: transitive + dependency: "direct main" description: name: analyzer sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" From 92179fe61cb7ae7282de4996b2e82a980c50fdf0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 15:16:06 +0800 Subject: [PATCH 329/384] chore: do not get workspace usage when current user is not owner --- .../menu/sidebar_sections_bloc.dart | 5 ++- .../billing/settings_billing_bloc.dart | 2 +- .../settings/plan/settings_plan_bloc.dart | 7 +++-- .../sidebar/billing/sidebar_plan_bloc.dart | 31 +++++++++++-------- .../application/sidebar/space/space_bloc.dart | 5 ++- .../workspace/workspace_service.dart | 18 +++++++++-- frontend/appflowy_flutter/test/util.dart | 5 ++- .../rust-lib/flowy-user/src/event_handler.rs | 5 ++- .../user_manager/manager_user_workspace.rs | 7 +++-- 9 files changed, 61 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart index 5a20b29c09..d6a6a73578 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -244,7 +244,10 @@ class SidebarSectionsBloc } void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService(workspaceId: workspaceId); + _workspaceService = WorkspaceService( + workspaceId: workspaceId, + userId: userProfile.id, + ); _listener = WorkspaceSectionsListener( user: userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart index ab324df87f..df880891e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart @@ -29,7 +29,7 @@ class SettingsBillingBloc required Int64 userId, }) : super(const _Initial()) { _userService = UserBackendService(userId: userId); - _service = WorkspaceService(workspaceId: workspaceId); + _service = WorkspaceService(workspaceId: workspaceId, userId: userId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart index dcc5156937..26975b00ff 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart @@ -23,7 +23,10 @@ class SettingsPlanBloc extends Bloc { required this.workspaceId, required Int64 userId, }) : super(const _Initial()) { - _service = WorkspaceService(workspaceId: workspaceId); + _service = WorkspaceService( + workspaceId: workspaceId, + userId: userId, + ); _userService = UserBackendService(userId: userId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); @@ -43,7 +46,7 @@ class SettingsPlanBloc extends Bloc { FlowyError? error; final usageResult = snapshots.first.fold( - (s) => s as WorkspaceUsagePB, + (s) => s as WorkspaceUsagePB?, (f) { error = f; return null; diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart index 2fae0e4872..56d6ae8cc8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/file_storage/file_storage_listener.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; @@ -182,20 +183,24 @@ class SidebarPlanBloc extends Bloc { ); } - void _checkWorkspaceUsage() { - if (state.workspaceId != null) { - final payload = UserWorkspaceIdPB(workspaceId: state.workspaceId!); - UserEventGetWorkspaceUsage(payload).send().then((result) { - result.onSuccess( - (usage) { - if (isClosed) { - return; - } - add(SidebarPlanEvent.updateWorkspaceUsage(usage)); - }, - ); - }); + Future _checkWorkspaceUsage() async { + if (state.workspaceId == null || state.userProfile == null) { + return; } + + await WorkspaceService( + workspaceId: state.workspaceId!, + userId: state.userProfile!.id, + ).getWorkspaceUsage().then((result) { + result.fold( + (usage) { + if (!isClosed && usage != null) { + add(SidebarPlanEvent.updateWorkspaceUsage(usage)); + } + }, + (error) => Log.error("Failed to get workspace usage: $error"), + ); + }); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index d4c1b4fe16..6d6ce05051 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -486,7 +486,10 @@ class SpaceBloc extends Bloc { } void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService(workspaceId: workspaceId); + _workspaceService = WorkspaceService( + workspaceId: workspaceId, + userId: userProfile.id, + ); this.userProfile = userProfile; this.workspaceId = workspaceId; diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index b958e5cd30..ae6220994e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -1,15 +1,18 @@ import 'dart:async'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:fixnum/fixnum.dart' as fixnum; class WorkspaceService { - WorkspaceService({required this.workspaceId}); + WorkspaceService({required this.workspaceId, required this.userId}); final String workspaceId; + final fixnum.Int64 userId; Future> createView({ required String name, @@ -82,7 +85,18 @@ class WorkspaceService { return FolderEventMoveView(payload).send(); } - Future> getWorkspaceUsage() { + Future> getWorkspaceUsage() async { + final request = WorkspaceMemberIdPB()..uid = userId; + final result = await UserEventGetMemberInfo(request).send(); + final isOwner = result.fold( + (member) => member.role.isOwner, + (_) => false, + ); + + if (!isOwner) { + return FlowyResult.success(null); + } + final payload = UserWorkspaceIdPB(workspaceId: workspaceId); return UserEventGetWorkspaceUsage(payload).send(); } diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index c4f2a21c64..3bb774411b 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -69,7 +69,10 @@ class AppFlowyUnitTest { } Future _initialServices() async { - workspaceService = WorkspaceService(workspaceId: currentWorkspace.id); + workspaceService = WorkspaceService( + workspaceId: currentWorkspace.id, + userId: userProfile.id, + ); } Future createWorkspace() async { diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 0e42525d04..85b9274df3 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -896,7 +896,10 @@ pub async fn get_workspace_member_info( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let member = manager.get_workspace_member_info(param.uid).await?; + let workspace_id = manager.get_session()?.user_workspace.workspace_id()?; + let member = manager + .get_workspace_member_info(param.uid, &workspace_id) + .await?; data_result_ok(member.into()) } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 496019cd2e..507916acf3 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -583,8 +583,11 @@ impl UserManager { Ok(UseAISettingPB::from(settings)) } - pub async fn get_workspace_member_info(&self, uid: i64) -> FlowyResult { - let workspace_id = self.get_session()?.user_workspace.workspace_id()?; + pub async fn get_workspace_member_info( + &self, + uid: i64, + workspace_id: &Uuid, + ) -> FlowyResult { let db = self.authenticate_user.get_sqlite_connection(uid)?; // Can opt in using memory cache if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { From f727dde74bd97d3be961251c2a4524346395ed7e Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 16 Apr 2025 15:59:43 +0800 Subject: [PATCH 330/384] fix: lose nested children when accepting AI responses (#7760) * chore: add gitkeep in assets/font dir * Revert "feat: improve white label scripts on Windows (#7755)" This reverts commit a5eb2cdd9a0171ecbc442aef09a8cf8db469a214. * chore: use --verbose * fix: lose nested children when accept ai response * chore: lock analyzer version * Reapply "feat: improve white label scripts on Windows (#7755)" This reverts commit c73186306eaf3e9f78114fcc29e39e3a2bce528d. * chore: lock analyzer version * chore: update editor version --- .../appflowy_flutter/assets/fonts/.gitkeep | 0 .../ai/operations/ai_writer_cubit.dart | 2 +- .../operations/ai_writer_node_extension.dart | 12 +++- .../base/markdown_text_robot.dart | 56 +++++++++++++++++-- .../packages/flowy_infra/pubspec.yaml | 1 + .../packages/flowy_infra_ui/pubspec.yaml | 2 + frontend/appflowy_flutter/pubspec.lock | 6 +- frontend/appflowy_flutter/pubspec.yaml | 18 +++--- .../freezed/generate_freezed.sh | 8 +-- frontend/scripts/code_generation/generate.sh | 2 +- 10 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/fonts/.gitkeep diff --git a/frontend/appflowy_flutter/assets/fonts/.gitkeep b/frontend/appflowy_flutter/assets/fonts/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 7b9da81d44..4bc13321b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -19,7 +19,7 @@ import 'ai_writer_node_extension.dart'; /// Enable the debug log for the AiWriterCubit. /// /// This is useful for debugging the AI writer cubit. -const _aiWriterCubitDebugLog = false; +const _aiWriterCubitDebugLog = true; class AiWriterCubit extends Cubit { AiWriterCubit({ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 6f4456f9ec..881871b154 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -34,7 +34,15 @@ extension AiWriterNodeExtension on EditorState { // if the selected nodes are not entirely selected, slice the nodes final slicedNodes = []; - final nodes = getNodesInSelection(selection); + final List flattenNodes = getNodesInSelection(selection); + final List nodes = []; + + for (final node in flattenNodes) { + if (nodes.any((element) => element.isParentOf(node))) { + continue; + } + nodes.add(node); + } for (final node in nodes) { final delta = node.delta; @@ -76,7 +84,7 @@ extension AiWriterNodeExtension on EditorState { // using \n will cause the ai response treat the text as a single line final markdown = await customDocumentToMarkdown( Document.blank()..insert([0], slicedNodes), - lineBreak: '\n\n', + lineBreak: '\n', ); // trim the last \n if it exists diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 0dea3d2864..259777db94 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -398,6 +398,15 @@ class MarkdownTextRobot { // it means the user selected the entire sentence, we just replace the node if (startIndex == 0 && length == node.delta?.length) { + if (nodes.isNotEmpty && node.children.isNotEmpty) { + // merge the children of the selected node and the first node of the ai response + nodes[0] = nodes[0].copyWith( + children: [ + ...node.children.map((e) => e.deepCopy()), + ...nodes[0].children, + ], + ); + } transaction ..insertNodes(node.path.next, nodes) ..deleteNode(node); @@ -441,7 +450,14 @@ class MarkdownTextRobot { ).root.children; // Get the selected nodes. - final nodes = editorState.getNodesInSelection(selection); + final flattenNodes = editorState.getNodesInSelection(selection); + final nodes = []; + for (final node in flattenNodes) { + if (nodes.any((element) => element.isParentOf(node))) { + continue; + } + nodes.add(node); + } // Note: Don't change its order, otherwise the delta will be incorrect. // step 1. merge the first selected node and the first node from the ai response @@ -465,9 +481,23 @@ class MarkdownTextRobot { transaction ..deleteText(firstNode, startIndex, length) ..insertTextDelta(firstNode, startIndex, firstMarkdownDelta); + + // if the first markdown node has children, we need to insert the children + // and delete the children of the first node that are in the selection. + if (firstMarkdownNode.children.isNotEmpty) { + transaction.insertNodes( + firstNode.path.child(0), + firstMarkdownNode.children.map((e) => e.deepCopy()), + ); + } + + final nodesToDelete = + firstNode.children.where((e) => e.path.inSelection(selection)); + transaction.deleteNodes(nodesToDelete); } // step 2 + bool handledLastNode = false; final lastNode = nodes.lastOrNull; final lastDelta = lastNode?.delta; final lastMarkdownNode = markdownNodes.lastOrNull; @@ -475,7 +505,10 @@ class MarkdownTextRobot { if (lastNode != null && lastDelta != null && lastMarkdownNode != null && - lastMarkdownDelta != null) { + lastMarkdownDelta != null && + firstNode?.id != lastNode.id) { + handledLastNode = true; + final endIndex = selection.endIndex; transaction.deleteText(lastNode, 0, endIndex); @@ -484,15 +517,30 @@ class MarkdownTextRobot { // selected text in the first node. if (lastMarkdownNode.id != firstMarkdownNode?.id) { transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta); + + if (lastMarkdownNode.children.isNotEmpty) { + transaction + ..insertNodes( + lastNode.path.child(0), + lastMarkdownNode.children.map((e) => e.deepCopy()), + ) + ..deleteNodes( + lastNode.children.where((e) => e.path.inSelection(selection)), + ); + } } } // step 3 final insertedPath = selection.start.path.nextNPath(1); - if (markdownNodes.length > 2) { + final insertLength = handledLastNode ? 2 : 1; + if (markdownNodes.length > insertLength) { transaction.insertNodes( insertedPath, - markdownNodes.skip(1).take(markdownNodes.length - 2).toList(), + markdownNodes + .skip(1) + .take(markdownNodes.length - insertLength) + .toList(), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 478c649664..9bf0245dc0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: freezed_annotation: ^2.1.0 file_picker: ^8.0.2 file: ^7.0.0 + analyzer: 6.11.0 dev_dependencies: build_runner: ^2.4.9 diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index 62cb26d4e0..b5b5c22bc7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -31,6 +31,8 @@ dependencies: flowy_svg: path: ../flowy_svg + analyzer: 6.11.0 + dev_dependencies: build_runner: ^2.4.9 provider: ^6.0.5 diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index cc574f7e64..063b770ce3 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -15,7 +15,7 @@ packages: source: sdk version: "0.3.3" analyzer: - dependency: transitive + dependency: "direct main" description: name: analyzer sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4967ed5" - resolved-ref: "4967ed57d9190948c08f868972c0babfdc470ba7" + ref: "2361899" + resolved-ref: "23618990b2f4ab88df67d50598b2b53cd6853e0a" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8fe956a194..3ca505e7a5 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -2,13 +2,13 @@ name: appflowy description: Bring projects, wikis, and teams together with AI. AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data. The best open source alternative to Notion. -publish_to: 'none' +publish_to: "none" version: 0.8.9 environment: - flutter: '>=3.27.4' - sdk: '>=3.3.0 <4.0.0' + flutter: ">=3.27.4" + sdk: ">=3.3.0 <4.0.0" dependencies: any_date: ^1.0.4 @@ -39,7 +39,7 @@ dependencies: calendar_view: git: url: https://github.com/Xazin/flutter_calendar_view - ref: '6fe0c98' + ref: "6fe0c98" collection: ^1.17.1 connectivity_plus: ^5.0.2 cross_file: ^0.3.4+1 @@ -75,7 +75,7 @@ dependencies: flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: '355aa56' + ref: "355aa56" flutter_math_fork: ^0.7.3 flutter_slidable: ^3.0.0 @@ -152,6 +152,8 @@ dependencies: talker_bloc_logger: ^4.7.1 talker: ^4.7.1 + analyzer: 6.11.0 + dev_dependencies: # Introduce talker to log the bloc events, and only log the events in the development mode @@ -185,13 +187,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: '4967ed5' + ref: "2361899" appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git - path: 'packages/appflowy_editor_plugins' - ref: '4efcff7' + path: "packages/appflowy_editor_plugins" + ref: "4efcff7" sheet: git: diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 45de7573d7..216a01b232 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -66,9 +66,9 @@ if [ "$exclude_packages" = false ]; then fi fi if [ "$verbose" = true ]; then - dart run build_runner build + dart run build_runner build --delete-conflicting-outputs else - dart run build_runner build >/dev/null 2>&1 + dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 fi echo "🧊 Done generating freezed files ($d)." fi @@ -108,9 +108,9 @@ fi # Start the build_runner in the background if [ "$verbose" = true ]; then - dart run build_runner build -d & + dart run build_runner build --delete-conflicting-outputs & else - dart run build_runner build -d >/dev/null 2>&1 & + dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 & fi # Get the PID of the background process diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index e08bc873fd..fd2edab785 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -64,7 +64,7 @@ cd .. cd freezed # Allow execution permissions on CI chmod +x ./generate_freezed.sh -./generate_freezed.sh "${args[@]}" --show-loading +./generate_freezed.sh "${args[@]}" --show-loading --verbose # Return to the original directory cd "$original_dir" From f6522297184bccc02c7c259d635bb86e0b6790f2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 16:08:11 +0800 Subject: [PATCH 331/384] fix: open relation database row --- .../row/related_row_detail_bloc.dart | 31 +++++++++---------- .../flowy-database2/src/event_handler.rs | 20 ++++++++---- .../rust-lib/flowy-database2/src/event_map.rs | 6 ++-- .../rust-lib/flowy-database2/src/manager.rs | 9 ++++++ 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart index 4f975cd1a6..f735618dd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart @@ -73,27 +73,24 @@ class RelatedRowDetailPageBloc }); } - /// initialize bloc through the `database_id` and `row_id`. The process is as - /// follows: - /// 1. use the `database_id` to get the database meta, which contains the - /// `inline_view_id` - /// 2. use the `inline_view_id` to instantiate a `DatabaseController`. - /// 3. use the `row_id` with the DatabaseController` to create `RowController` void _init(String databaseId, String initialRowId) async { - final databaseMeta = - await DatabaseEventGetDatabaseMeta(DatabaseIdPB(value: databaseId)) - .send() - .fold((s) => s, (f) => null); - if (databaseMeta == null) { + final viewId = await DatabaseEventGetDefaultDatabaseViewId( + DatabaseIdPB(value: databaseId), + ).send().fold( + (pb) => pb.value, + (error) => null, + ); + + if (viewId == null) { return; } - final inlineView = - await ViewBackendService.getView(databaseMeta.inlineViewId) - .fold((viewPB) => viewPB, (f) => null); - if (inlineView == null) { + + final databaseView = await ViewBackendService.getView(viewId) + .fold((viewPB) => viewPB, (f) => null); + if (databaseView == null) { return; } - final databaseController = DatabaseController(view: inlineView); + final databaseController = DatabaseController(view: databaseView); await databaseController.open().fold( (s) => databaseController.setIsLoading(false), (f) => null, @@ -104,7 +101,7 @@ class RelatedRowDetailPageBloc } final rowController = RowController( rowMeta: rowInfo.rowMeta, - viewId: inlineView.id, + viewId: databaseView.id, rowCache: databaseController.rowCache, ); diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 6117a8ee8a..9164550fe4 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -865,17 +865,25 @@ pub(crate) async fn delete_group_handler( } #[tracing::instrument(level = "debug", skip(manager), err)] -pub(crate) async fn get_database_meta_handler( +pub(crate) async fn get_default_database_view_id_handler( data: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let manager = upgrade_manager(manager)?; let database_id = data.into_inner().value; - let inline_view_id = manager.get_database_inline_view_id(&database_id).await?; + let database_view_id = manager + .get_database_meta(&database_id) + .await? + .and_then(|mut d| d.linked_views.pop()) + .ok_or_else(|| { + FlowyError::internal().with_context(format!( + "Can't find any database view for given database id: {}", + database_id + )) + })?; - let data = DatabaseMetaPB { - database_id, - inline_view_id, + let data = DatabaseViewIdPB { + value: database_view_id, }; data_result_ok(data) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 6281cde745..824565e5b8 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -64,7 +64,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::CreateGroup, create_group_handler) .event(DatabaseEvent::DeleteGroup, delete_group_handler) // Database - .event(DatabaseEvent::GetDatabaseMeta, get_database_meta_handler) + .event(DatabaseEvent::GetDefaultDatabaseViewId, get_default_database_view_id_handler) .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) @@ -305,8 +305,8 @@ pub enum DatabaseEvent { #[event(input = "DeleteGroupPayloadPB")] DeleteGroup = 115, - #[event(input = "DatabaseIdPB", output = "DatabaseMetaPB")] - GetDatabaseMeta = 119, + #[event(input = "DatabaseIdPB", output = "DatabaseViewIdPB")] + GetDefaultDatabaseViewId = 119, /// Returns all the databases #[event(output = "RepeatedDatabaseDescriptionPB")] diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index b2b2b0a676..49a946d108 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -166,6 +166,15 @@ impl DatabaseManager { items } + pub async fn get_database_meta(&self, database_id: &str) -> FlowyResult> { + let mut database_meta = None; + if let Some(lock) = self.workspace_database_manager.load_full() { + let wdb = lock.read().await; + database_meta = wdb.get_database_meta(database_id); + } + Ok(database_meta) + } + #[instrument(level = "trace", skip_all, err)] pub async fn update_database_indexing( &self, From 0be8dcc000e6d2e21ac212b617bdf7a8933c2b2f Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 16:29:28 +0800 Subject: [PATCH 332/384] chore: remove duplciate --- frontend/appflowy_flutter/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index eb7fd6b755..3ca505e7a5 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -11,7 +11,6 @@ environment: sdk: ">=3.3.0 <4.0.0" dependencies: - analyzer: 6.11.0 any_date: ^1.0.4 app_links: ^6.3.3 appflowy_backend: From 079112c9d2343b50af547781eeb9715310fa7bae Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 16:30:07 +0800 Subject: [PATCH 333/384] chore: clippy --- .../flowy-user/src/user_manager/manager_user_workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 507916acf3..dae5a51ccc 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -606,7 +606,7 @@ impl UserManager { } let member = self - .get_workspace_member_info_from_remote(&workspace_id, uid) + .get_workspace_member_info_from_remote(workspace_id, uid) .await?; Ok(member) From 13acc3af860cfa7c1dbebb9562a1d16a8cf90c7b Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 16 Apr 2025 16:56:37 +0800 Subject: [PATCH 334/384] fix: auto focus on link name textfield after open the link_edit_menu (#7757) * fix: auto focus on link name textfield after open the link_edit_menu * chore: update pubspec.yaml --- .../desktop_toolbar/link/link_edit_menu.dart | 164 +++++++++++------- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 100 insertions(+), 70 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart index b6a4ad89ba..e90ee22a80 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart @@ -13,6 +13,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; // ignore: implementation_imports import 'package:appflowy_editor/src/editor/util/link_util.dart'; +import 'package:flutter/services.dart'; import 'link_create_menu.dart'; import 'link_search_text_field.dart'; import 'link_styles.dart'; @@ -38,15 +39,14 @@ class LinkEditMenu extends StatefulWidget { } class _LinkEditMenuState extends State { - ValueChanged get onApply => widget.onApply; - ValueChanged get onRemoveLink => widget.onRemoveLink; VoidCallback get onDismiss => widget.onDismiss; late TextEditingController linkNameController = TextEditingController(text: linkInfo.name); - final textFocusNode = FocusNode(); + late FocusNode textFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); + late FocusNode menuFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); late LinkInfo linkInfo = widget.linkInfo; late LinkSearchTextField searchTextField; bool isShowingSearchResult = false; @@ -74,12 +74,14 @@ class _LinkEditMenuState extends State { if (mounted) setState(() {}); }, )..searchRecentViews(); + makeSureHasFocus(); } @override void dispose() { linkNameController.dispose(); textFocusNode.dispose(); + menuFocusNode.dispose(); searchTextField.dispose(); super.dispose(); } @@ -91,56 +93,59 @@ class _LinkEditMenuState extends State { final errorHeight = showErrorText ? 20.0 : 0.0; return GestureDetector( onTap: onDismiss, - child: Container( - width: 400, - height: 250 + (showingRecent ? 32 : 0), - color: Colors.white.withAlpha(1), - child: Stack( - children: [ - GestureDetector( - onTap: hideSearchResult, - child: Container( - width: 400, - height: 192 + errorHeight, - decoration: buildToolbarLinkDecoration(context), + child: Focus( + focusNode: menuFocusNode, + child: Container( + width: 400, + height: 250 + (showingRecent ? 32 : 0), + color: Colors.white.withAlpha(1), + child: Stack( + children: [ + GestureDetector( + onTap: hideSearchResult, + child: Container( + width: 400, + height: 192 + errorHeight, + decoration: buildToolbarLinkDecoration(context), + ), ), - ), - Positioned( - top: 16, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_pageOrURL.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, + Positioned( + top: 16, + left: 20, + child: FlowyText.semibold( + LocaleKeys.document_toolbar_pageOrURL.tr(), + color: LinkStyle.textTertiary, + fontSize: 12, + figmaLineHeight: 16, + ), ), - ), - Positioned( - top: 80 + errorHeight, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_linkName.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, + Positioned( + top: 80 + errorHeight, + left: 20, + child: FlowyText.semibold( + LocaleKeys.document_toolbar_linkName.tr(), + color: LinkStyle.textTertiary, + fontSize: 12, + figmaLineHeight: 16, + ), ), - ), - Positioned( - top: 144 + errorHeight, - left: 20, - child: buildButtons(), - ), - Positioned( - top: 100 + errorHeight, - left: 20, - child: buildNameTextField(), - ), - Positioned( - top: 36, - left: 20, - child: buildLinkField(), - ), - ], + Positioned( + top: 144 + errorHeight, + left: 20, + child: buildButtons(), + ), + Positioned( + top: 100 + errorHeight, + left: 20, + child: buildNameTextField(), + ), + Positioned( + top: 36, + left: 20, + child: buildLinkField(), + ), + ], + ), ), ), ); @@ -254,23 +259,7 @@ class _LinkEditMenuState extends State { fontColor: Colors.white, fillColor: LinkStyle.fillThemeThick, fontWeight: FontWeight.w400, - onPressed: () { - if (isShowingSearchResult) { - onConfirm(); - return; - } - if (linkInfo.link.isEmpty) { - widget.onRemoveLink(linkInfo); - return; - } - if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onApply.call(linkInfo); - }, + onPressed: onApply, ); }, ), @@ -287,6 +276,7 @@ class _LinkEditMenuState extends State { child: TextFormField( autovalidateMode: AutovalidateMode.onUserInteraction, focusNode: textFocusNode, + autofocus: true, textAlign: TextAlign.left, controller: linkNameController, style: TextStyle( @@ -395,6 +385,45 @@ class _LinkEditMenuState extends State { ); } + KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) { + if (key is! KeyDownEvent) return KeyEventResult.ignored; + if (key.logicalKey == LogicalKeyboardKey.enter) { + onApply(); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.escape) { + onDismiss(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + + Future makeSureHasFocus() async { + final focusNode = textFocusNode; + if (!mounted || focusNode.hasFocus) return; + focusNode.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + makeSureHasFocus(); + }); + } + + void onApply() { + if (isShowingSearchResult) { + onConfirm(); + return; + } + if (linkInfo.link.isEmpty) { + widget.onRemoveLink(linkInfo); + return; + } + if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) { + setState(() { + showErrorText = true; + }); + return; + } + widget.onApply.call(linkInfo); + } + void onConfirm() { searchTextField.onSearchResult( onLink: onLinkSelected, @@ -404,6 +433,7 @@ class _LinkEditMenuState extends State { searchTextField.unfocus(); }, ); + menuFocusNode.requestFocus(); } Future getPageView() async { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 063b770ce3..c871a41f7e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2361899" - resolved-ref: "23618990b2f4ab88df67d50598b2b53cd6853e0a" + ref: "680222f" + resolved-ref: "680222f503f90d07c08c99c42764f9b08fd0f46c" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 3ca505e7a5..e8042d6a57 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -187,7 +187,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "2361899" + ref: "680222f" appflowy_editor_plugins: git: From c89f33e2f89f5cc82b3c3ff5b72f2f8b7e683605 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 20:59:25 +0800 Subject: [PATCH 335/384] chore: remove unused code --- .../application/workspace_error_bloc.dart | 15 +--- .../screens/workspace_error_screen.dart | 41 --------- frontend/rust-lib/Cargo.lock | 29 +------ frontend/rust-lib/flowy-ai/src/chat.rs | 3 +- frontend/rust-lib/flowy-ai/src/entities.rs | 4 + frontend/rust-lib/flowy-core/src/lib.rs | 9 ++ .../rust-lib/flowy-core/src/server_layer.rs | 32 +------ frontend/rust-lib/flowy-server/Cargo.toml | 12 +-- .../flowy-server/src/af_cloud/define.rs | 7 +- .../af_cloud/impls/user/cloud_service_impl.rs | 4 - .../src/local_server/impls/chat.rs | 6 +- .../src/local_server/impls/folder.rs | 6 +- .../src/local_server/impls/user.rs | 84 +++++++------------ .../flowy-server/src/local_server/server.rs | 58 +++++-------- .../flowy-server/tests/af_cloud_test/util.rs | 14 +++- frontend/rust-lib/flowy-user-pub/Cargo.toml | 1 + frontend/rust-lib/flowy-user-pub/src/cloud.rs | 2 - .../rust-lib/flowy-user/src/event_handler.rs | 18 ---- frontend/rust-lib/flowy-user/src/event_map.rs | 4 - .../src/services/authenticate_user.rs | 20 ----- .../src/services/sqlite_sql/user_sql.rs | 7 -- .../flowy-user/src/user_manager/manager.rs | 1 - .../user_manager/manager_user_workspace.rs | 18 ---- 23 files changed, 101 insertions(+), 294 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart index ce51fdd10b..7ff50dbd02 100644 --- a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart @@ -1,9 +1,7 @@ import 'package:appflowy/plugins/database/application/defines.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -22,20 +20,10 @@ class WorkspaceErrorBloc void _dispatch() { on( (event, emit) async { - await event.when( + event.when( init: () { // _loadSnapshots(); }, - resetWorkspace: () async { - emit(state.copyWith(loadingState: const LoadingState.loading())); - final payload = ResetWorkspacePB.create() - ..workspaceId = userFolder.workspaceId - ..uid = userFolder.uid; - final result = await UserEventResetWorkspace(payload).send(); - if (!isClosed) { - add(WorkspaceErrorEvent.didResetWorkspace(result)); - } - }, didResetWorkspace: (result) { result.fold( (_) { @@ -68,7 +56,6 @@ class WorkspaceErrorBloc class WorkspaceErrorEvent with _$WorkspaceErrorEvent { const factory WorkspaceErrorEvent.init() = _Init; const factory WorkspaceErrorEvent.logout() = _DidLogout; - const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace; const factory WorkspaceErrorEvent.didResetWorkspace( FlowyResult result, ) = _DidResetWorkspace; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index d79127e04c..bd32696514 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -86,7 +86,6 @@ class WorkspaceErrorScreen extends StatelessWidget { const VSpace(50), const LogoutButton(), const VSpace(20), - const ResetWorkspaceButton(), ]); return Center( @@ -157,43 +156,3 @@ class LogoutButton extends StatelessWidget { ); } } - -class ResetWorkspaceButton extends StatelessWidget { - const ResetWorkspaceButton({super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 200, - height: 40, - child: BlocBuilder( - builder: (context, state) { - final isLoading = state.loadingState?.isLoading() ?? false; - final icon = isLoading - ? const Center( - child: CircularProgressIndicator.adaptive(), - ) - : null; - - return FlowyButton( - text: FlowyText.medium( - LocaleKeys.workspace_reset.tr(), - textAlign: TextAlign.center, - ), - onTap: () { - NavigatorAlertDialog( - title: LocaleKeys.workspace_resetWorkspacePrompt.tr(), - confirm: () { - context.read().add( - const WorkspaceErrorEvent.resetWorkspace(), - ); - }, - ).show(context); - }, - rightIcon: icon, - ); - }, - ), - ); - } -} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 8e46148ea9..ac47524944 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2957,7 +2957,6 @@ dependencies = [ "arc-swap", "assert-json-diff", "bytes", - "chrono", "client-api", "collab", "collab-database", @@ -2966,7 +2965,6 @@ dependencies = [ "collab-folder", "collab-plugins", "collab-user", - "dashmap 6.0.1", "dotenv", "flowy-ai-pub", "flowy-database-pub", @@ -2975,33 +2973,25 @@ dependencies = [ "flowy-folder-pub", "flowy-search-pub", "flowy-server-pub", + "flowy-sqlite", "flowy-storage", "flowy-storage-pub", "flowy-user-pub", "futures", "futures-util", - "hex", - "hyper 0.14.27", "lazy_static", - "lib-dispatch", "lib-infra", - "mime_guess", - "postgrest", "rand 0.8.5", - "reqwest 0.11.27", "semver", "serde", "serde_json", "thiserror 1.0.64", "tokio", - "tokio-retry", "tokio-stream", "tokio-util", "tracing", "tracing-subscriber", - "url", "uuid", - "yrs", ] [[package]] @@ -3138,6 +3128,7 @@ name = "flowy-user-pub" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "base64 0.21.5", "chrono", "client-api", @@ -5382,15 +5373,6 @@ dependencies = [ "postgres-protocol", ] -[[package]] -name = "postgrest" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" -dependencies = [ - "reqwest 0.11.27", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -6130,7 +6112,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.2", "winreg 0.50.0", ] @@ -8443,12 +8424,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - [[package]] name = "webpki-roots" version = "0.26.7" diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 5be2b7b014..a8655ff647 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -581,6 +581,7 @@ impl Chat { author_type: record.author_type, author_id: record.author_id, reply_message_id: record.reply_message_id, + metadata: record.metadata, }) .collect::>(); @@ -641,7 +642,7 @@ fn save_chat_message_disk( author_type: message.author.author_type as i64, author_id: message.author.author_id.to_string(), reply_message_id: message.reply_message_id, - metadata: None, + metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), }) .collect::>(); insert_chat_messages(conn, &records)?; diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index d0cb7eda02..b62899eca3 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -296,6 +296,9 @@ pub struct ChatMessagePB { #[pb(index = 6, one_of)] pub reply_message_id: Option, + + #[pb(index = 7, one_of)] + pub metadata: Option, } #[derive(Debug, Clone, Default, ProtoBuf)] @@ -316,6 +319,7 @@ impl From for ChatMessagePB { author_type: chat_message.author.author_type as i64, author_id: chat_message.author.author_id.to_string(), reply_message_id: None, + metadata: Some(serde_json::to_string(&chat_message.metadata).unwrap_or_default()), } } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 31b98da5e6..21f09c1dad 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -35,6 +35,7 @@ use crate::deps_resolve::*; use crate::log_filter::init_log; use crate::server_layer::{current_server_type, Server, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; +use flowy_sqlite::DBConnection; use user_state_callback::UserStatusCallbackImpl; pub mod config; @@ -334,4 +335,12 @@ impl ServerUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } + + fn user_id(&self) -> FlowyResult { + self.upgrade_user()?.user_id() + } + + fn get_sqlite_db(&self, uid: i64) -> Result { + self.upgrade_user()?.get_sqlite_connection(uid) + } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 0d304c6063..8157748b4f 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,15 +1,15 @@ use arc_swap::ArcSwapOption; use dashmap::DashMap; +use diesel::Connection; +use serde_repr::*; use std::fmt::{Display, Formatter}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -use serde_repr::*; - use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; -use flowy_server::local_server::{LocalServer, LocalServerDB}; +use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; @@ -115,10 +115,7 @@ impl ServerProvider { let server = match server_type { Server::Local => { - let local_db = Arc::new(LocalServerDBImpl { - storage_path: self.config.storage_path.clone(), - }); - let server = Arc::new(LocalServer::new(local_db)); + let server = Arc::new(LocalServer::new(self.user.clone())); Ok::, FlowyError>(server) }, Server::AppFlowyCloud => { @@ -171,24 +168,3 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } - -struct LocalServerDBImpl { - #[allow(dead_code)] - storage_path: String, -} - -impl LocalServerDB for LocalServerDBImpl { - fn get_user_profile(&self, _uid: i64) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("LocalServer doesn't support get_user_profile"), - ) - } - - fn get_user_workspace(&self, _uid: i64) -> Result, FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("LocalServer doesn't support get_user_workspace"), - ) - } -} diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 9e67081eb7..e94570cc4a 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -12,20 +12,15 @@ crate-type = ["cdylib", "rlib"] tracing.workspace = true futures.workspace = true futures-util = "0.3.26" -reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } -hyper = "0.14" serde.workspace = true serde_json.workspace = true thiserror = "1.0" tokio = { workspace = true, features = ["sync"] } lazy_static = "1.4.0" bytes = { workspace = true, features = ["serde"] } -tokio-retry = "0.3" anyhow.workspace = true arc-swap.workspace = true -dashmap.workspace = true uuid.workspace = true -chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } collab = { workspace = true } collab-plugins = { workspace = true } collab-document = { workspace = true } @@ -33,8 +28,6 @@ collab-entity = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } collab-user = { workspace = true } -hex = "0.4.3" -postgrest = "1.0" lib-infra = { workspace = true } flowy-user-pub = { workspace = true } flowy-folder-pub = { workspace = true } @@ -46,14 +39,11 @@ flowy-search-pub = { workspace = true } flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-ai-pub = { workspace = true } -mime_guess = "2.0" -url = "2.4" tokio-util = "0.7" tokio-stream = { workspace = true, features = ["sync"] } -lib-dispatch = { workspace = true } -yrs.workspace = true rand = "0.8.5" semver = "1.0.23" +flowy-sqlite = { workspace = true } [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index db75ea93fe..3b2895b8f8 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,4 +1,5 @@ -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::DBConnection; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -10,4 +11,8 @@ pub const USER_DEVICE_ID: &str = "device_id"; pub trait ServerUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; + + fn user_id(&self) -> FlowyResult; + + fn get_sqlite_db(&self, uid: i64) -> Result; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index e59166fc37..6d7d9d743b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -378,10 +378,6 @@ where Arc::into_inner(rx) } - async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { - Ok(()) - } - async fn create_collab_object( &self, collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index a68dbc8b4f..a6bfea53b4 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,3 +1,4 @@ +use crate::af_cloud::define::ServerUser; use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, @@ -10,9 +11,12 @@ use lib_infra::util::timestamp; use serde_json::Value; use std::collections::HashMap; use std::path::Path; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerChatServiceImpl; +pub struct LocalServerChatServiceImpl { + pub user: Arc, +} #[async_trait] impl ChatCloudService for LocalServerChatServiceImpl { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 7bb3139953..bd2372e9d4 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,7 +1,6 @@ #![allow(unused_variables)] use std::sync::Arc; -use crate::local_server::LocalServerDB; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab_entity::CollabType; @@ -14,10 +13,7 @@ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; use uuid::Uuid; -pub(crate) struct LocalServerFolderCloudServiceImpl { - #[allow(dead_code)] - pub db: Arc, -} +pub(crate) struct LocalServerFolderCloudServiceImpl; #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 8d9e342e85..df9050623e 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -18,24 +18,19 @@ use lib_infra::box_any::BoxAny; use lib_infra::util::timestamp; use crate::local_server::uid::UserIDGenerator; -use crate::local_server::LocalServerDB; lazy_static! { //FIXME: seriously, userID generation should work using lock-free algorithm static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserAuthServiceImpl { - #[allow(dead_code)] - pub db: Arc, -} - +pub(crate) struct LocalServerUserServiceImpl; #[async_trait] -impl UserCloudService for LocalServerUserAuthServiceImpl { +impl UserCloudService for LocalServerUserServiceImpl { async fn sign_up(&self, params: BoxAny) -> Result { let params = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let workspace_id = uuid::Uuid::new_v4().to_string(); + let workspace_id = Uuid::new_v4().to_string(); let user_workspace = UserWorkspace::new_local(&workspace_id, uid); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() @@ -58,13 +53,9 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { } async fn sign_in(&self, params: BoxAny) -> Result { - let db = self.db.clone(); let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - - let user_workspace = db - .get_user_workspace(uid)? - .unwrap_or_else(make_user_workspace); + let user_workspace = make_user_workspace(); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -132,16 +123,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { } async fn get_user_profile(&self, credential: UserCredentials) -> Result { - match credential.uid { - None => Err(FlowyError::record_not_found()), - Some(uid) => { - self.db.get_user_profile(uid).map(|mut profile| { - // We don't want to expose the email in the local server - profile.email = "".to_string(); - profile - }) - }, - } + Err(FlowyError::local_version_not_support().with_context("Not support")) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { @@ -155,6 +137,32 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { Ok(vec![]) } + async fn create_workspace(&self, _workspace_name: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + + async fn patch_workspace( + &self, + workspace_id: &Uuid, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + + async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + async fn get_user_awareness_doc_state( &self, uid: i64, @@ -172,10 +180,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { Ok(encode_collab.doc_state.to_vec()) } - async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { - Ok(()) - } - async fn create_collab_object( &self, _collab_object: &CollabObject, @@ -194,32 +198,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { .with_context("local server doesn't support batch create collab object"), ) } - - async fn create_workspace(&self, _workspace_name: &str) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } - - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } - - async fn patch_workspace( - &self, - workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, - ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } } fn make_user_workspace() -> UserWorkspace { diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 280a56cfe0..282d118203 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,40 +1,30 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; -use flowy_document_pub::cloud::DocumentCloudService; -use flowy_error::FlowyError; -use flowy_folder_pub::cloud::FolderCloudService; -use flowy_storage_pub::cloud::StorageCloudService; -use tokio::sync::mpsc; -// use flowy_user::services::database::{ -// get_user_profile, get_user_workspace, open_collab_db, open_user_db, -// }; -use flowy_user_pub::cloud::UserCloudService; -use flowy_user_pub::entities::*; - +use crate::af_cloud::define::ServerUser; use crate::local_server::impls::{ LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, - LocalServerUserAuthServiceImpl, + LocalServerUserServiceImpl, }; use crate::AppFlowyServer; - -pub trait LocalServerDB: Send + Sync + 'static { - fn get_user_profile(&self, uid: i64) -> Result; - fn get_user_workspace(&self, uid: i64) -> Result, FlowyError>; -} +use flowy_ai_pub::cloud::ChatCloudService; +use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; +use flowy_document_pub::cloud::DocumentCloudService; +use flowy_folder_pub::cloud::FolderCloudService; +use flowy_storage_pub::cloud::StorageCloudService; +use flowy_user_pub::cloud::UserCloudService; +use tokio::sync::mpsc; pub struct LocalServer { - local_db: Arc, + user: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(local_db: Arc) -> Self { + pub fn new(user: Arc) -> Self { Self { - local_db, + user, stop_tx: Default::default(), } } @@ -49,38 +39,36 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { - Arc::new(LocalServerUserAuthServiceImpl { - db: self.local_db.clone(), - }) + Arc::new(LocalServerUserServiceImpl) } fn folder_service(&self) -> Arc { - Arc::new(LocalServerFolderCloudServiceImpl { - db: self.local_db.clone(), - }) + Arc::new(LocalServerFolderCloudServiceImpl) } fn database_service(&self) -> Arc { Arc::new(LocalServerDatabaseCloudServiceImpl()) } + fn database_ai_service(&self) -> Option> { + None + } + fn document_service(&self) -> Arc { Arc::new(LocalServerDocumentCloudServiceImpl()) } - fn file_storage(&self) -> Option> { - None + fn chat_service(&self) -> Arc { + Arc::new(LocalServerChatServiceImpl { + user: self.user.clone(), + }) } fn search_service(&self) -> Option> { None } - fn database_ai_service(&self) -> Option> { + fn file_storage(&self) -> Option> { None } - - fn chat_service(&self) -> Arc { - Arc::new(LocalServerChatServiceImpl) - } } diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 9c88917df8..d00f484068 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -3,14 +3,14 @@ use semver::Version; use std::collections::HashMap; use std::sync::Arc; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; +use crate::setup_log; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; - -use crate::setup_log; +use flowy_sqlite::DBConnection; /// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: /// @@ -42,6 +42,14 @@ impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } + + fn user_id(&self) -> FlowyResult { + todo!() + } + + fn get_sqlite_db(&self, uid: i64) -> Result { + todo!() + } } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index 0228e25d35..80d087e88e 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -23,3 +23,4 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } +arc-swap = "1.7.1" diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 3f7f39910b..dde94aebdc 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -263,8 +263,6 @@ pub trait UserCloudService: Send + Sync + 'static { None } - async fn reset_workspace(&self, collab_object: CollabObject) -> Result<(), FlowyError>; - async fn create_collab_object( &self, collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 85b9274df3..c0b7a8d6c2 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -614,24 +614,6 @@ pub async fn get_all_reminder_event_handler( data_result_ok(reminders.into()) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn reset_workspace_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let reset_pb = data.into_inner(); - if reset_pb.workspace_id.is_empty() { - return Err(FlowyError::new( - ErrorCode::WorkspaceInitializeError, - "The workspace id is empty", - )); - } - let _session = manager.get_session()?; - manager.reset_workspace(reset_pb).await?; - Ok(()) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub async fn remove_reminder_event_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 2de1fdfbdc..7ed4948771 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -49,7 +49,6 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetAllReminders, get_all_reminder_event_handler) .event(UserEvent::RemoveReminder, remove_reminder_event_handler) .event(UserEvent::UpdateReminder, update_reminder_event_handler) - .event(UserEvent::ResetWorkspace, reset_workspace_handler) .event(UserEvent::SetDateTimeSettings, set_date_time_settings) .event(UserEvent::GetDateTimeSettings, get_date_time_settings) .event(UserEvent::SetNotificationSettings, set_notification_settings) @@ -183,9 +182,6 @@ pub enum UserEvent { #[event(input = "ReminderPB")] UpdateReminder = 31, - #[event(input = "ResetWorkspacePB")] - ResetWorkspace = 32, - /// Change the Date/Time formats globally #[event(input = "DateTimeSettingsPB")] SetDateTimeSettings = 33, diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index ab9a35b483..bc9603e26a 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -1,7 +1,6 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; -use crate::services::sqlite_sql::user_sql::vacuum_database; use collab_integrate::CollabKVDB; use arc_swap::ArcSwapOption; @@ -18,8 +17,6 @@ use std::sync::{Arc, Weak}; use tracing::{error, info}; use uuid::Uuid; -const SQLITE_VACUUM_042: &str = "sqlite_vacuum_042_version"; - pub struct AuthenticateUser { pub user_config: UserConfig, pub(crate) database: Arc, @@ -44,23 +41,6 @@ impl AuthenticateUser { } } - pub fn vacuum_database_if_need(&self) { - if !self - .store_preferences - .get_bool_or_default(SQLITE_VACUUM_042) - { - if let Ok(session) = self.get_session() { - let _ = self.store_preferences.set_bool(SQLITE_VACUUM_042, true); - if let Ok(conn) = self.database.get_connection(session.user_id) { - info!("vacuum database 042"); - if let Err(err) = vacuum_database(conn) { - error!("vacuum database error: {:?}", err); - } - } - } - } - } - pub fn user_id(&self) -> FlowyResult { let session = self.get_session()?; Ok(session.user_id) diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index 6da6f183cb..c63764a055 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -148,10 +148,3 @@ pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result Result<(), FlowyError> { - sql_query("VACUUM") - .execute(&mut *conn) - .map_err(internal_error)?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index b4bc8911e1..5b5d060539 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -268,7 +268,6 @@ impl UserManager { }, _ => error!("Failed to get collab db or sqlite pool"), } - self.authenticate_user.vacuum_database_if_need(); // migrations should run before set the first time installed version self.set_first_time_installed_version(); diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index dae5a51ccc..2fff0c260b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -414,24 +414,6 @@ impl UserManager { Ok(workspaces) } - /// Reset the remote workspace using local workspace data. This is useful when a user wishes to - /// open a workspace on a new device that hasn't fully synchronized with the server. - pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> { - let collab_object = CollabObject::new( - reset.uid, - reset.workspace_id.clone(), - CollabType::Folder, - reset.workspace_id.clone(), - self.authenticate_user.user_config.device_id.clone(), - ); - self - .cloud_services - .get_user_service()? - .reset_workspace(collab_object) - .await?; - Ok(()) - } - #[instrument(level = "info", skip(self), err)] pub async fn subscribe_workspace( &self, From 77fbf0f8a355bca2caaf123dd6b60e6ad06ab643 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 21:26:09 +0800 Subject: [PATCH 336/384] chore: local ai initialize --- frontend/rust-lib/Cargo.lock | 2 + frontend/rust-lib/Cargo.toml | 2 + frontend/rust-lib/flowy-ai/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/lib.rs | 2 +- .../flowy-ai/src/local_ai/controller.rs | 117 +++++++++++------- frontend/rust-lib/flowy-core/Cargo.toml | 3 +- .../rust-lib/flowy-core/src/server_layer.rs | 27 ++-- frontend/rust-lib/flowy-server/Cargo.toml | 1 + 8 files changed, 98 insertions(+), 60 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ac47524944..2e85c57326 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2609,6 +2609,7 @@ name = "flowy-core" version = "0.1.0" dependencies = [ "af-local-ai", + "af-plugin", "anyhow", "arc-swap", "base64 0.21.5", @@ -2966,6 +2967,7 @@ dependencies = [ "collab-plugins", "collab-user", "dotenv", + "flowy-ai", "flowy-ai-pub", "flowy-database-pub", "flowy-document-pub", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 581715985f..6e93b13a19 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -99,6 +99,8 @@ zip = "2.2.0" dashmap = "6.0.1" derive_builder = "0.20.2" tantivy = { version = "0.24.0" } +af-plugin = { version = "0.1" } +af-local-ai = { version = "0.1" } # Please using the following command to update the revision id # Current directory: frontend diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index a31d42da61..3a6aaf5898 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -35,8 +35,8 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -af-local-ai = { version = "0.1.0" } -af-plugin = { version = "0.1.0" } +af-local-ai = { workspace = true } +af-plugin = { workspace = true } reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" base64 = "0.21.5" diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 6ab100fd6e..ccd3920e0d 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -5,7 +5,7 @@ pub mod ai_manager; mod chat; mod completion; pub mod entities; -mod local_ai; +pub mod local_ai; // #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] // pub mod mcp; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index ac44e9ad55..494e11d073 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -24,10 +24,10 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument}; +use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -53,7 +53,7 @@ pub struct LocalAIController { ai_plugin: Arc, resource: Arc, current_chat_id: ArcSwapOption, - store_preferences: Arc, + store_preferences: Weak, user_service: Arc, #[allow(dead_code)] cloud_service: Arc, @@ -70,7 +70,7 @@ impl Deref for LocalAIController { impl LocalAIController { pub fn new( plugin_manager: Arc, - store_preferences: Arc, + store_preferences: Weak, user_service: Arc, cloud_service: Arc, ) -> Self { @@ -94,7 +94,7 @@ impl LocalAIController { let mut running_state_rx = local_ai.subscribe_running_state(); let cloned_llm_res = Arc::clone(&local_ai_resource); - let cloned_store_preferences = Arc::clone(&store_preferences); + let cloned_store_preferences = store_preferences.clone(); let cloned_local_ai = Arc::clone(&local_ai); let cloned_user_service = Arc::clone(&user_service); @@ -110,44 +110,47 @@ impl LocalAIController { info!("[AI Plugin] state: {:?}", state); // Read whether plugin is enabled from store; default to true - let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + if let Some(store_preferences) = cloned_store_preferences.upgrade() { + let enabled = store_preferences.get_bool(&key).unwrap_or(true); + // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled + let (plugin_downloaded, lack_of_resource) = + if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { + // Possibly check plugin readiness and resource concurrency in parallel, + // but here we do it sequentially for clarity. + let downloaded = is_plugin_ready(); + let resource_lack = cloned_llm_res.get_lack_of_resource().await; + (downloaded, resource_lack) + } else { + (false, None) + }; - // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled - let (plugin_downloaded, lack_of_resource) = - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - // Possibly check plugin readiness and resource concurrency in parallel, - // but here we do it sequentially for clarity. - let downloaded = is_plugin_ready(); - let resource_lack = cloned_llm_res.get_lack_of_resource().await; - (downloaded, resource_lack) + // If plugin is running, retrieve version + let plugin_version = if matches!(state, RunningState::Running { .. }) { + match cloned_local_ai.plugin_info().await { + Ok(info) => Some(info.version), + Err(_) => None, + } } else { - (false, None) + None }; - // If plugin is running, retrieve version - let plugin_version = if matches!(state, RunningState::Running { .. }) { - match cloned_local_ai.plugin_info().await { - Ok(info) => Some(info.version), - Err(_) => None, - } + // Broadcast the new local AI state + let new_state = RunningStatePB::from(state); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + plugin_downloaded, + lack_of_resource, + state: new_state, + plugin_version, + }) + .send(); } else { - None - }; - - // Broadcast the new local AI state - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version, - }) - .send(); + warn!("[AI Plugin] store preferences is dropped"); + } } }); @@ -207,6 +210,13 @@ impl LocalAIController { Ok(()) } + fn upgrade_store_preferences(&self) -> FlowyResult> { + self + .store_preferences + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) + } + /// Indicate whether the local AI plugin is running. pub fn is_running(&self) -> bool { if !self.is_enabled() { @@ -228,7 +238,10 @@ impl LocalAIController { .workspace_id() .map(|workspace_id| local_ai_enabled_key(&workspace_id)) { - self.store_preferences.get_bool(&key).unwrap_or(false) + match self.upgrade_store_preferences() { + Ok(store) => store.get_bool(&key).unwrap_or(false), + Err(_) => false, + } } else { false } @@ -373,8 +386,9 @@ impl LocalAIController { pub async fn toggle_local_ai(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let key = local_ai_enabled_key(&workspace_id); - let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); - self.store_preferences.set_bool(&key, enabled)?; + let store_preferences = self.upgrade_store_preferences()?; + let enabled = !store_preferences.get_bool(&key).unwrap_or(true); + store_preferences.set_bool(&key, enabled)?; self.toggle_plugin(enabled).await?; Ok(enabled) } @@ -591,7 +605,16 @@ async fn initialize_ai_plugin( pub struct LLMResourceServiceImpl { user_service: Arc, cloud_service: Arc, - store_preferences: Arc, + store_preferences: Weak, +} + +impl LLMResourceServiceImpl { + fn upgrade_store_preferences(&self) -> FlowyResult> { + self + .store_preferences + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) + } } #[async_trait] impl LLMResourceService for LLMResourceServiceImpl { @@ -605,16 +628,14 @@ impl LLMResourceService for LLMResourceServiceImpl { } fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { - self - .store_preferences - .set_object(LOCAL_AI_SETTING_KEY, &setting)?; + let store_preferences = self.upgrade_store_preferences()?; + store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; Ok(()) } fn retrieve_setting(&self) -> Option { - self - .store_preferences - .get_object::(LOCAL_AI_SETTING_KEY) + let store_preferences = self.upgrade_store_preferences().ok()?; + store_preferences.get_object::(LOCAL_AI_SETTING_KEY) } } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 6499f79284..b4e7bd5fec 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -37,7 +37,8 @@ flowy-storage-pub = { workspace = true } client-api.workspace = true flowy-ai = { workspace = true } flowy-ai-pub = { workspace = true } -af-local-ai = { version = "0.1.0" } +af-local-ai = { workspace = true } +af-plugin = { workspace = true } tracing.workspace = true diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 8157748b4f..176818abdf 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,11 +1,9 @@ +use crate::AppFlowyCoreConfig; +use af_plugin::manager::PluginManager; use arc_swap::ArcSwapOption; use dashmap::DashMap; -use diesel::Connection; -use serde_repr::*; -use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; -use std::sync::{Arc, Weak}; - +use flowy_ai::ai_manager::AIUserService; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; @@ -14,8 +12,10 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; - -use crate::AppFlowyCoreConfig; +use serde_repr::*; +use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::{Arc, Weak}; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -66,10 +66,21 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, + user_service: impl AIUserService, server_user: impl ServerUser + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); + + let user_service = Arc::new(user_service); + let plugin_manager = Arc::new(PluginManager::new()); + let local_ai = Arc::new(LocalAIController::new( + plugin_manager.clone(), + store_preferences.clone(), + user_service.clone(), + chat_cloud_service.clone(), + )); + Self { config, providers: DashMap::new(), diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index e94570cc4a..5225eb817d 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -44,6 +44,7 @@ tokio-stream = { workspace = true, features = ["sync"] } rand = "0.8.5" semver = "1.0.23" flowy-sqlite = { workspace = true } +flowy-ai = { workspace = true } [dependencies.client-api] workspace = true From e2896b29117b7f51eb805ff0c718d3f6057e19ce Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 22:03:14 +0800 Subject: [PATCH 337/384] chore: remove inline view id reference --- .../cell/bloc/relation_cell_bloc.dart | 5 ++- .../relation_type_option_cubit.dart | 10 +++--- .../cell_editor/relation_cell_editor.dart | 2 +- frontend/rust-lib/Cargo.lock | 34 ++++++------------- frontend/rust-lib/Cargo.toml | 16 ++++----- .../src/entities/database_entities.rs | 5 +-- .../flowy-database2/src/event_handler.rs | 2 +- .../rust-lib/flowy-database2/src/manager.rs | 9 ----- .../src/services/database/database_editor.rs | 4 +-- .../src/services/share/csv/export.rs | 8 +++-- 10 files changed, 34 insertions(+), 61 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart index 70c5e074ab..ec789b03a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart @@ -143,12 +143,11 @@ class RelationCellBloc extends Bloc { (f) => null, ); if (databaseMeta != null) { - final result = - await ViewBackendService.getView(databaseMeta.inlineViewId); + final result = await ViewBackendService.getView(databaseMeta.viewId); return result.fold( (s) => DatabaseMeta( databaseId: databaseId, - inlineViewId: databaseMeta.inlineViewId, + viewId: databaseMeta.viewId, databaseName: s.name, ), (f) => null, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart index 691b6b7227..4ddde80b79 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart @@ -17,11 +17,11 @@ class RelationDatabaseListCubit extends Cubit { .send() .fold>((s) => s.items, (f) => []); final futures = metaPBs.map((meta) { - return ViewBackendService.getView(meta.inlineViewId).then( + return ViewBackendService.getView(meta.viewId).then( (result) => result.fold( (s) => DatabaseMeta( databaseId: meta.databaseId, - inlineViewId: meta.inlineViewId, + viewId: meta.viewId, databaseName: s.name, ), (f) => null, @@ -43,10 +43,10 @@ class DatabaseMeta with _$DatabaseMeta { /// id of the database required String databaseId, - /// id of the inline view - required String inlineViewId, + /// id of the view + required String viewId, - /// name of the database, currently identical to the name of the inline view + /// name of the database required String databaseName, }) = _DatabaseMeta; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index e68e77cd97..7f6960de9d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -256,7 +256,7 @@ class _CellEditorTitle extends StatelessWidget { } void _openRelatedDatbase(BuildContext context) { - FolderEventGetView(ViewIdPB(value: databaseMeta.inlineViewId)) + FolderEventGetView(ViewIdPB(value: databaseMeta.viewId)) .send() .then((result) { result.fold( diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a84de4cdd6..4ae0275383 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "bytes", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-recursion", @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-stream", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "collab", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,7 +5209,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5277,19 +5276,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 88972e5bfb..1efaa0cc97 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -141,14 +141,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 8c16db4379..2562bd84f7 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -26,9 +26,6 @@ pub struct DatabasePB { #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, - - #[pb(index = 5)] - pub is_linked: bool, } #[derive(ProtoBuf, Default)] @@ -208,7 +205,7 @@ pub struct DatabaseMetaPB { pub database_id: String, #[pb(index = 2)] - pub inline_view_id: String, + pub view_id: String, } #[derive(Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 9164550fe4..63d6fdf2c3 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -900,7 +900,7 @@ pub(crate) async fn get_databases_handler( if let Some(link_view) = meta.linked_views.first() { items.push(DatabaseMetaPB { database_id: meta.database_id, - inline_view_id: link_view.clone(), + view_id: link_view.clone(), }) } } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 49a946d108..a7cca9960b 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -148,15 +148,6 @@ impl DatabaseManager { Ok(()) } - pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { - let lock = self.workspace_database()?; - let wdb = lock.read().await; - let database_collab = wdb.get_or_init_database(database_id).await?; - drop(wdb); - let lock_guard = database_collab.read().await; - Ok(lock_guard.get_inline_view_id()) - } - pub async fn get_all_databases_meta(&self) -> Vec { let mut items = vec![]; if let Some(lock) = self.workspace_database_manager.load_full() { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index d079bdc8c2..227b96df4f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1505,7 +1505,7 @@ impl DatabaseEditor { view_editor.set_row_orders(row_orders.clone()).await; // Collect database details in a single block holding the `read` lock - let (database_id, fields, is_linked) = { + let (database_id, fields) = { let database = self.database.read().await; ( database.get_database_id(), @@ -1514,7 +1514,6 @@ impl DatabaseEditor { .into_iter() .map(FieldIdPB::from) .collect::>(), - database.is_inline_view(view_id), ) }; @@ -1557,7 +1556,6 @@ impl DatabaseEditor { fields, rows: order_rows, layout_type: view_layout.into(), - is_linked, }); // Mark that the opening process is complete if let Some(tx) = self.is_loading_rows.load_full() { diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index dd704f43d5..3eab243fd7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -28,8 +28,10 @@ impl CSVExport { style: CSVFormat, ) -> FlowyResult { let mut wtr = csv::Writer::from_writer(vec![]); - let inline_view_id = database.get_inline_view_id(); - let fields = database.get_fields_in_view(&inline_view_id, None); + let view_id = database + .get_first_database_view_id() + .ok_or_else(|| FlowyError::internal().with_context("failed to get first database view"))?; + let fields = database.get_fields_in_view(&view_id, None); // Write fields let field_records = fields @@ -49,7 +51,7 @@ impl CSVExport { field_by_field_id.insert(field.id.clone(), field); }); let rows = database - .get_rows_for_view(&inline_view_id, 20, None) + .get_rows_for_view(&view_id, 20, None) .await .filter_map(|result| async { result.ok() }) .collect::>() From 8f6366728284c5ee42c9113bfdaa40ffbf3bdfcf Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 17 Apr 2025 09:48:23 +0800 Subject: [PATCH 338/384] feat: upgrade ubuntu version to 22.04 in github action (#7764) * feat: upgrade ubuntu version to 22.04 in github action * fix: cargo clippy --- .github/workflows/release.yml | 2 +- .../link_preview/link_parsers/default_parser.dart | 5 ++--- .../flowy-user/src/user_manager/manager_user_workspace.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 918a2018f7..a4582ffa74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -338,7 +338,7 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, + os: ubuntu-22.04, extra-build-args: "", flutter_profile: production-linux-x86_64, } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart index f05f4a9706..ab0b246743 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart @@ -1,9 +1,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; import 'package:appflowy_backend/log.dart'; - -import 'package:http/http.dart' as http; // ignore: depend_on_referenced_packages import 'package:html/parser.dart' as html_parser; +import 'package:http/http.dart' as http; abstract class LinkInfoParser { Future parse( @@ -70,7 +69,7 @@ class DefaultParser implements LinkInfoParser { } final favicon = - 'https://www.google.com/s2/favicons?sz=64&domain=${link.host}'; + 'https://www.faviconextractor.com/favicon/${link.host}?larger=true'; return LinkInfo( url: '$link', diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index dae5a51ccc..adc1b72266 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -593,7 +593,7 @@ impl UserManager { if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self - .get_workspace_member_info_from_remote(&workspace_id, uid) + .get_workspace_member_info_from_remote(workspace_id, uid) .await?; } From fbf928e6e481ca7e40e9a1ed211b6681aaedd7fa Mon Sep 17 00:00:00 2001 From: Morn Date: Thu, 17 Apr 2025 11:01:30 +0800 Subject: [PATCH 339/384] chore: update CHANGELOG.md (#7765) --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 289f7fd004..a5e7e268a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,24 @@ # Release Notes +## Version 0.8.9 - 16/04/2025 +### Desktop +#### New Features +- Supported pasting a link as a mention, providing a more condensed visualization of linked content +- Supported converting between link formats (e.g. transforming a mention into a bookmark) +- Improved the link editing experience with enhanced UX +- Added OTP (One-Time Password) support for sign-in authentication +- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet +#### Bug Fixes +- Fixed an issue where properties were not displaying in the row detail page +- Fixed a bug where Undo didn't work in the row detail page +- Fixed an issue where blocks didn't grow when the grid got bigger +- Fixed several bugs related to AI writers +### Mobile +#### New Features +- Added sign-in with OTP (One-Time Password) +#### Bug Fixes +- Fixed an issue where the slash menu sometimes failed to display +- Updated the mention page block to handle page selection with more context. + ## Version 0.8.8 - 01/04/2025 ### New Features - Added support for selecting AI models in AI writer From c633dd0919304a160f6ac06e0c523da3354f5797 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 11:11:54 +0800 Subject: [PATCH 340/384] chore: remove unused code --- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 8 +-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 14 +---- frontend/rust-lib/flowy-ai/src/chat.rs | 3 +- .../flowy-ai/src/local_ai/controller.rs | 21 +------ .../flowy-ai/src/local_ai/resource.rs | 20 ------ .../src/middleware/chat_service_mw.rs | 61 ++++++++++--------- .../flowy-core/src/deps_resolve/chat_deps.rs | 7 +-- .../src/deps_resolve/cloud_service_impl.rs | 15 ++--- frontend/rust-lib/flowy-core/src/lib.rs | 8 +++ .../rust-lib/flowy-core/src/server_layer.rs | 30 +++++++-- .../flowy-server/src/af_cloud/define.rs | 2 + .../flowy-server/src/af_cloud/impls/chat.rs | 23 +------ .../src/local_server/impls/chat.rs | 10 +-- .../src/local_server/impls/folder.rs | 1 - .../src/local_server/impls/user.rs | 1 - .../flowy-server/tests/af_cloud_test/util.rs | 5 ++ .../src/services/authenticate_user.rs | 2 +- .../src/services/sqlite_sql/user_sql.rs | 4 +- .../user_manager/manager_user_workspace.rs | 5 +- 19 files changed, 97 insertions(+), 143 deletions(-) diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 79478f64fc..5e9923d464 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -141,6 +141,7 @@ pub trait ChatCloudService: Send + Sync + 'static { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result; async fn stream_complete( @@ -158,13 +159,6 @@ pub trait ChatCloudService: Send + Sync + 'static { metadata: Option>, ) -> Result<(), FlowyError>; - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result; - - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError>; - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index ec12ac4963..d619b8ab7e 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -8,7 +8,6 @@ use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; -use af_plugin::manager::PluginManager; use dashmap::DashMap; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, @@ -35,7 +34,6 @@ use uuid::Uuid; pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; - fn device_id(&self) -> Result; fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; @@ -85,16 +83,9 @@ impl AIManager { store_preferences: Arc, storage_service: Weak, query_service: impl AIExternalService, + local_ai: Arc, ) -> AIManager { let user_service = Arc::new(user_service); - let plugin_manager = Arc::new(PluginManager::new()); - let local_ai = Arc::new(LocalAIController::new( - plugin_manager.clone(), - store_preferences.clone(), - user_service.clone(), - chat_cloud_service.clone(), - )); - let cloned_local_ai = local_ai.clone(); tokio::spawn(async move { cloned_local_ai.observe_plugin_resource().await; @@ -596,7 +587,8 @@ impl AIManager { message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; - let resp = chat.get_related_question(message_id).await?; + let ai_model = self.get_active_model(&chat_id.to_string()).await; + let resp = chat.get_related_question(message_id, ai_model).await?; Ok(resp) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index a8655ff647..a496e067eb 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -524,11 +524,12 @@ impl Chat { pub async fn get_related_question( &self, message_id: i64, + ai_model: Option, ) -> Result { let workspace_id = self.user_service.workspace_id()?; let resp = self .chat_service - .get_related_message(&workspace_id, &self.chat_id, message_id) + .get_related_message(&workspace_id, &self.chat_id, message_id, ai_model) .await?; trace!( diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 494e11d073..431d8ff681 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -6,7 +6,7 @@ use crate::notification::{ }; use af_plugin::manager::PluginManager; use anyhow::Error; -use flowy_ai_pub::cloud::{ChatCloudService, ChatMessageMetadata, ContextLoader, LocalAIConfig}; +use flowy_ai_pub::cloud::{ChatMessageMetadata, ContextLoader}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; @@ -55,8 +55,6 @@ pub struct LocalAIController { current_chat_id: ArcSwapOption, store_preferences: Weak, user_service: Arc, - #[allow(dead_code)] - cloud_service: Arc, } impl Deref for LocalAIController { @@ -72,7 +70,6 @@ impl LocalAIController { plugin_manager: Arc, store_preferences: Weak, user_service: Arc, - cloud_service: Arc, ) -> Self { debug!( "[AI Plugin] init local ai controller, thread: {:?}", @@ -83,7 +80,6 @@ impl LocalAIController { let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), - cloud_service: cloud_service.clone(), store_preferences: store_preferences.clone(), }; let local_ai_resource = Arc::new(LocalAIResourceController::new( @@ -160,7 +156,6 @@ impl LocalAIController { current_chat_id: ArcSwapOption::default(), store_preferences, user_service, - cloud_service, } } #[instrument(level = "debug", skip_all)] @@ -379,10 +374,6 @@ impl LocalAIController { .map(|path| path.to_string_lossy().to_string()) } - pub async fn get_plugin_download_link(&self) -> FlowyResult { - self.resource.get_plugin_download_link().await - } - pub async fn toggle_local_ai(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let key = local_ai_enabled_key(&workspace_id); @@ -604,7 +595,6 @@ async fn initialize_ai_plugin( pub struct LLMResourceServiceImpl { user_service: Arc, - cloud_service: Arc, store_preferences: Weak, } @@ -618,15 +608,6 @@ impl LLMResourceServiceImpl { } #[async_trait] impl LLMResourceService for LLMResourceServiceImpl { - async fn fetch_local_ai_config(&self) -> Result { - let workspace_id = self.user_service.workspace_id()?; - let config = self - .cloud_service - .get_local_ai_config(&workspace_id) - .await?; - Ok(config) - } - fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { let store_preferences = self.upgrade_store_preferences()?; store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index f2c1d1d041..6251ef8de5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -1,6 +1,5 @@ use crate::ai_manager::AIUserService; use crate::local_ai::controller::LocalAISetting; -use flowy_ai_pub::cloud::LocalAIConfig; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; @@ -33,7 +32,6 @@ struct ModelEntry { #[async_trait] pub trait LLMResourceService: Send + Sync + 'static { /// Get local ai configuration from remote server - async fn fetch_local_ai_config(&self) -> Result; fn store_setting(&self, setting: LocalAISetting) -> Result<(), anyhow::Error>; fn retrieve_setting(&self) -> Option; } @@ -125,11 +123,6 @@ impl LocalAIResourceController { .is_ok_and(|r| r.is_none()) } - pub async fn get_plugin_download_link(&self) -> FlowyResult { - let ai_config = self.get_local_ai_configuration().await?; - Ok(ai_config.plugin.url) - } - /// Retrieves model information and updates the current model settings. pub fn get_llm_setting(&self) -> LocalAISetting { self.resource_service.retrieve_setting().unwrap_or_default() @@ -271,19 +264,6 @@ impl LocalAIResourceController { Ok(config) } - /// Fetches the local AI configuration from the resource service. - async fn get_local_ai_configuration(&self) -> FlowyResult { - self - .resource_service - .fetch_local_ai_config() - .await - .map_err(|err| { - error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); - FlowyError::local_ai() - .with_context("Can't retrieve model info. Please try again later".to_string()) - }) - } - pub(crate) fn user_model_folder(&self) -> FlowyResult { self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 8c27268139..54f1c929dd 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -10,9 +10,9 @@ use std::collections::HashMap; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, LocalAIConfig, - MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, - ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, + RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, + StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; @@ -50,10 +50,6 @@ impl AICloudServiceMiddleware { } } - pub fn is_local_ai_enabled(&self) -> bool { - self.local_ai.is_enabled() - } - pub async fn index_message_metadata( &self, chat_id: &Uuid, @@ -63,7 +59,7 @@ impl AICloudServiceMiddleware { if metadata_list.is_empty() { return Ok(()); } - if self.is_local_ai_enabled() { + if self.local_ai.is_enabled() { let _ = index_process_sink .send(StreamMessage::IndexStart.to_string()) .await; @@ -262,28 +258,41 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); + let use_local_ai = match &ai_model { + None => false, + Some(model) => model.is_local, + }; - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, + if use_local_ai { + if self.local_ai.is_running() { + let questions = self + .local_ai + .get_related_question(&chat_id.to_string()) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + trace!("LocalAI related questions: {:?}", questions); + + let items = questions + .into_iter() + .map(|content| RelatedQuestion { + content, + metadata: None, + }) + .collect::>(); + + Ok(RepeatedRelatedQuestion { message_id, items }) + } else { + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], }) - .collect::>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) + } } else { self .cloud_service - .get_related_message(workspace_id, chat_id, message_id) + .get_related_message(workspace_id, chat_id, message_id, ai_model) .await } } @@ -359,10 +368,6 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - self.cloud_service.get_local_ai_config(workspace_id).await - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 2d9a57b331..a8280f9f66 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -6,6 +6,7 @@ use collab::util::is_change_since_sv; use collab_entity::CollabType; use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; use flowy_error::FlowyError; use flowy_folder::ViewLayout; @@ -33,6 +34,7 @@ impl ChatDepsResolver { storage_service: Weak, folder_cloud_service: Arc, folder_service: impl FolderService, + local_ai: Arc, ) -> Arc { let user_service = ChatUserServiceImpl(authenticate_user); Arc::new(AIManager::new( @@ -44,6 +46,7 @@ impl ChatDepsResolver { folder_service: Box::new(folder_service), folder_cloud_service, }, + local_ai, )) } } @@ -166,10 +169,6 @@ impl AIUserService for ChatUserServiceImpl { self.upgrade_user()?.user_id() } - fn device_id(&self) -> Result { - self.upgrade_user()?.device_id() - } - fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 836bd6b32b..30fa6d0ae4 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -15,8 +15,8 @@ use flowy_ai_pub::cloud::search_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, - StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, + StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -753,11 +753,12 @@ impl ChatCloudService for ServerProvider { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { self .get_server()? .chat_service() - .get_related_message(workspace_id, chat_id, message_id) + .get_related_message(workspace_id, chat_id, message_id, ai_model) .await } @@ -801,14 +802,6 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - self - .get_server()? - .chat_service() - .get_local_ai_config(workspace_id) - .await - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 21f09c1dad..7584628056 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -9,6 +9,7 @@ use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; use flowy_server::af_cloud::define::ServerUser; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; @@ -191,6 +192,7 @@ impl AppFlowyCore { Arc::downgrade(&storage_manager.storage_service), server_provider.clone(), folder_query_service.clone(), + server_provider.local_ai.clone(), ); let database_manager = DatabaseDepsResolver::resolve( @@ -343,4 +345,10 @@ impl ServerUser for ServerUserImpl { fn get_sqlite_db(&self, uid: i64) -> Result { self.upgrade_user()?.get_sqlite_connection(uid) } + + fn application_root_dir(&self) -> Result { + Ok(PathBuf::from( + self.upgrade_user()?.get_application_root_dir(), + )) + } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 176818abdf..0c7bc88859 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -11,11 +11,14 @@ use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; use serde_repr::*; use std::fmt::{Display, Formatter}; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; +use uuid::Uuid; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -59,6 +62,7 @@ pub struct ServerProvider { authenticator: AtomicU8, user: Arc, pub(crate) uid: Arc>, + pub local_ai: Arc, } impl ServerProvider { @@ -66,19 +70,16 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, - user_service: impl AIUserService, server_user: impl ServerUser + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); - - let user_service = Arc::new(user_service); + let user_service = Arc::new(AIUserServiceImpl(user.clone())); let plugin_manager = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( plugin_manager.clone(), store_preferences.clone(), user_service.clone(), - chat_cloud_service.clone(), )); Self { @@ -90,6 +91,7 @@ impl ServerProvider { store_preferences, uid: Default::default(), user, + local_ai, } } @@ -179,3 +181,23 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } + +struct AIUserServiceImpl(Arc); + +impl AIUserService for AIUserServiceImpl { + fn user_id(&self) -> Result { + self.0.user_id() + } + + fn workspace_id(&self) -> Result { + self.0.workspace_id() + } + + fn sqlite_connection(&self, uid: i64) -> Result { + self.0.get_sqlite_db(uid) + } + + fn application_root_dir(&self) -> Result { + self.0.application_root_dir() + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 3b2895b8f8..cf98cf5214 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,5 +1,6 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; +use std::path::PathBuf; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -15,4 +16,5 @@ pub trait ServerUser: Send + Sync { fn user_id(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; + fn application_root_dir(&self) -> Result; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 4081d659ba..a92cd8ffa9 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -9,12 +9,11 @@ use client_api::entity::chat_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - LocalAIConfig, ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; -use lib_infra::util::{get_operating_system, OperatingSystem}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; @@ -186,6 +185,7 @@ where workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? @@ -226,25 +226,6 @@ where ); } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - let system = get_operating_system(); - let platform = match system { - OperatingSystem::MacOS => "macos", - _ => { - return Err( - FlowyError::not_support() - .with_context("local ai is not supported on this operating system"), - ); - }, - }; - let config = self - .inner - .try_get_client()? - .get_local_ai_config(workspace_id.to_string().as_str(), platform) - .await?; - Ok(config) - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index a6bfea53b4..bd49ffb75b 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,5 +1,5 @@ use crate::af_cloud::define::ServerUser; -use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; +use client_api::entity::ai_dto::RepeatedRelatedQuestion; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, @@ -98,6 +98,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { _workspace_id: &Uuid, _chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { Ok(RepeatedRelatedQuestion { message_id, @@ -133,13 +134,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_local_ai_config(&self, _workspace_id: &Uuid) -> Result { - Err( - FlowyError::not_support() - .with_context("Get local ai config is not supported in local server."), - ) - } - async fn get_workspace_plan( &self, _workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index bd2372e9d4..31a65e7fa9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,5 +1,4 @@ #![allow(unused_variables)] -use std::sync::Arc; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index df9050623e..706e7f0597 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -5,7 +5,6 @@ use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; use lazy_static::lazy_static; -use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index d00f484068..b747c0b2ae 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -1,6 +1,7 @@ use client_api::ClientConfiguration; use semver::Version; use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use flowy_error::{FlowyError, FlowyResult}; @@ -50,6 +51,10 @@ impl ServerUser for FakeServerUserImpl { fn get_sqlite_db(&self, uid: i64) -> Result { todo!() } + + fn application_root_dir(&self) -> Result { + todo!() + } } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index bc9603e26a..7ec63f93f7 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -14,7 +14,7 @@ use flowy_user_pub::session::Session; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::{error, info}; +use tracing::info; use uuid::Uuid; pub struct AuthenticateUser { diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index c63764a055..e2b6628832 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -1,5 +1,5 @@ -use diesel::{sql_query, RunQueryDsl}; -use flowy_error::{internal_error, FlowyError}; +use diesel::RunQueryDsl; +use flowy_error::FlowyError; use std::str::FromStr; use flowy_user_pub::cloud::UserUpdate; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 2fff0c260b..b5ae54309b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -6,7 +6,6 @@ use std::convert::TryFrom; use std::str::FromStr; use std::sync::Arc; -use collab_entity::{CollabObject, CollabType}; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; @@ -20,7 +19,7 @@ use tracing::{error, info, instrument, trace, warn}; use uuid::Uuid; use crate::entities::{ - RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, + RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, UpdateUserWorkspaceSettingPB, UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB, }; use crate::migrations::AnonUser; @@ -575,7 +574,7 @@ impl UserManager { if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self - .get_workspace_member_info_from_remote(&workspace_id, uid) + .get_workspace_member_info_from_remote(workspace_id, uid) .await?; } From 98b835227ed59bb64b572ba886bef04aa7d038d4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 11:22:14 +0800 Subject: [PATCH 341/384] chore: remove unused code --- .../flowy-ai/src/middleware/chat_service_mw.rs | 7 ------- .../rust-lib/flowy-ai/src/persistence/chat_sql.rs | 1 - .../src/deps_resolve/cloud_service_impl.rs | 11 ----------- .../rust-lib/flowy-server/src/af_cloud/impls/chat.rs | 12 ------------ .../flowy-server/src/local_server/impls/chat.rs | 10 ---------- 5 files changed, 41 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 54f1c929dd..5a529d4201 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -368,13 +368,6 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - self.cloud_service.get_workspace_plan(workspace_id).await - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs index e962f2c880..45e839ae7e 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs @@ -76,7 +76,6 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< .execute(&mut *conn) } -#[allow(dead_code)] pub fn update_chat( conn: &mut SqliteConnection, changeset: ChatTableChangeset, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 30fa6d0ae4..a84aff211c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -802,17 +802,6 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - self - .get_server()? - .chat_service() - .get_workspace_plan(workspace_id) - .await - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index a92cd8ffa9..9bfd0e23fd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -226,18 +226,6 @@ where ); } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - let plans = self - .inner - .try_get_client()? - .get_active_workspace_subscriptions(workspace_id.to_string().as_str()) - .await?; - Ok(plans) - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index bd49ffb75b..fe90f45e77 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -134,16 +134,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_workspace_plan( - &self, - _workspace_id: &Uuid, - ) -> Result, FlowyError> { - Err( - FlowyError::not_support() - .with_context("Get local ai config is not supported in local server."), - ) - } - async fn get_chat_settings( &self, _workspace_id: &Uuid, From af91a72187a5bd199c3f95a951608b6493f48998 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 14:07:01 +0800 Subject: [PATCH 342/384] chore: select message --- frontend/rust-lib/Cargo.lock | 1 + frontend/rust-lib/flowy-ai-pub/Cargo.toml | 3 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 2 + frontend/rust-lib/flowy-ai-pub/src/lib.rs | 1 + .../src/persistence/chat_message_sql.rs | 35 +++-- .../src/persistence/chat_sql.rs | 67 +++++++-- .../src/persistence/mod.rs | 0 frontend/rust-lib/flowy-ai/src/ai_manager.rs | 33 +++-- frontend/rust-lib/flowy-ai/src/chat.rs | 30 ++-- frontend/rust-lib/flowy-ai/src/lib.rs | 1 - .../src/middleware/chat_service_mw.rs | 19 +-- .../src/deps_resolve/cloud_service_impl.rs | 4 +- .../rust-lib/flowy-core/src/server_layer.rs | 2 +- .../flowy-server/src/af_cloud/impls/chat.rs | 4 +- .../src/local_server/impls/chat.rs | 131 ++++++++++++++---- .../flowy-server/src/local_server/server.rs | 6 +- .../2025-04-17-042326_chat_metadata/down.sql | 9 ++ .../2025-04-17-042326_chat_metadata/up.sql | 4 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 26 ++-- 19 files changed, 276 insertions(+), 102 deletions(-) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/chat_message_sql.rs (75%) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/chat_sql.rs (66%) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/mod.rs (100%) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 2e85c57326..29547be823 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2564,6 +2564,7 @@ version = "0.1.0" dependencies = [ "client-api", "flowy-error", + "flowy-sqlite", "futures", "lib-infra", "serde", diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml index dfb67490ac..93ea79bcab 100644 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-ai-pub/Cargo.toml @@ -12,4 +12,5 @@ client-api = { workspace = true } futures.workspace = true serde_json.workspace = true serde.workspace = true -uuid.workspace = true \ No newline at end of file +uuid.workspace = true +flowy-sqlite = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 5e9923d464..0acf3c5bd3 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -85,6 +85,8 @@ pub trait ChatCloudService: Send + Sync + 'static { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError>; async fn create_question( diff --git a/frontend/rust-lib/flowy-ai-pub/src/lib.rs b/frontend/rust-lib/flowy-ai-pub/src/lib.rs index 1ede32218e..9a7423ec3f 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/lib.rs @@ -1 +1,2 @@ pub mod cloud; +pub mod persistence; diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs similarity index 75% rename from frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index 6eaf6798e3..ac3192064b 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -1,3 +1,4 @@ +use crate::cloud::MessageCursor; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::upsert::excluded; use flowy_sqlite::{ @@ -51,19 +52,25 @@ pub fn select_chat_messages( mut conn: DBConnection, chat_id_val: &str, limit_val: i64, - after_message_id: Option, - before_message_id: Option, + offset: MessageCursor, ) -> QueryResult> { let mut query = dsl::chat_message_table .filter(chat_message_table::chat_id.eq(chat_id_val)) .into_boxed(); - if let Some(after_message_id) = after_message_id { - query = query.filter(chat_message_table::message_id.gt(after_message_id)); + + match offset { + MessageCursor::AfterMessageId(after_message_id) => { + query = query.filter(chat_message_table::message_id.gt(after_message_id)); + }, + MessageCursor::BeforeMessageId(before_message_id) => { + query = query.filter(chat_message_table::message_id.lt(before_message_id)); + }, + MessageCursor::Offset(offset_val) => { + query = query.offset(offset_val as i64); + }, + MessageCursor::NextBack => {}, } - if let Some(before_message_id) = before_message_id { - query = query.filter(chat_message_table::message_id.lt(before_message_id)); - } query = query .order(( chat_message_table::created_at.desc(), @@ -75,7 +82,7 @@ pub fn select_chat_messages( Ok(messages) } -pub fn select_single_message( +pub fn select_message( mut conn: DBConnection, message_id_val: i64, ) -> QueryResult> { @@ -86,6 +93,18 @@ pub fn select_single_message( Ok(message) } +pub fn select_message_content( + mut conn: DBConnection, + message_id_val: i64, +) -> QueryResult> { + let message = dsl::chat_message_table + .filter(chat_message_table::message_id.eq(message_id_val)) + .select(chat_message_table::content) + .first::(&mut *conn) + .optional()?; + Ok(message) +} + pub fn select_message_where_match_reply_message_id( mut conn: DBConnection, answer_message_id_val: i64, diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs similarity index 66% rename from frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs index 45e839ae7e..218f6222c3 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs @@ -16,10 +16,8 @@ pub struct ChatTable { pub chat_id: String, pub created_at: i64, pub name: String, - pub local_files: String, pub metadata: String, - pub local_enabled: bool, - pub sync_to_cloud: bool, + pub rag_ids: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -49,22 +47,57 @@ pub struct ChatTableFile { pub struct ChatTableChangeset { pub chat_id: String, pub name: Option, - pub local_files: Option, pub metadata: Option, - pub local_enabled: Option, - pub sync_to_cloud: Option, + pub rag_ids: Option, } impl ChatTableChangeset { pub fn from_metadata(metadata: ChatTableMetadata) -> Self { ChatTableChangeset { + chat_id: Default::default(), metadata: serde_json::to_string(&metadata).ok(), - ..Default::default() + name: None, + rag_ids: None, + } + } + + pub fn from_rag_ids(rag_ids: Vec) -> Self { + ChatTableChangeset { + chat_id: Default::default(), + // Serialize the Vec to a JSON array string + rag_ids: Some(serde_json::to_string(&rag_ids).unwrap_or_default()), + name: None, + metadata: None, } } } -pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { +pub fn serialize_rag_ids(rag_ids: &[String]) -> String { + serde_json::to_string(rag_ids).unwrap_or_default() +} + +pub fn deserialize_rag_ids(rag_ids_str: &Option) -> Vec { + match rag_ids_str { + Some(str) => serde_json::from_str(str).unwrap_or_default(), + None => Vec::new(), + } +} + +pub fn deserialize_chat_metadata(metadata: &str) -> T +where + T: serde::de::DeserializeOwned + Default, +{ + serde_json::from_str(metadata).unwrap_or_default() +} + +pub fn serialize_chat_metadata(metadata: &T) -> String +where + T: Serialize, +{ + serde_json::to_string(metadata).unwrap_or_default() +} + +pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { diesel::insert_into(chat_table::table) .values(new_chat) .on_conflict(chat_table::chat_id) @@ -72,6 +105,8 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< .set(( chat_table::created_at.eq(excluded(chat_table::created_at)), chat_table::name.eq(excluded(chat_table::name)), + chat_table::metadata.eq(excluded(chat_table::metadata)), + chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), )) .execute(&mut *conn) } @@ -85,7 +120,6 @@ pub fn update_chat( Ok(affected_row) } -#[allow(dead_code)] pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { let row = dsl::chat_table .filter(chat_table::chat_id.eq(chat_id_val)) @@ -93,7 +127,17 @@ pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult FlowyResult> { + let chat = dsl::chat_table + .filter(chat_table::chat_id.eq(chat_id_val)) + .first::(conn)?; + + Ok(deserialize_rag_ids(&chat.rag_ids)) +} + pub fn read_chat_metadata( conn: &mut SqliteConnection, chat_id_val: &str, @@ -102,8 +146,7 @@ pub fn read_chat_metadata( .select(chat_table::metadata) .filter(chat_table::chat_id.eq(chat_id_val)) .first::(&mut *conn)?; - let value = serde_json::from_str(&metadata_str).unwrap_or_default(); - Ok(value) + Ok(deserialize_chat_metadata(&metadata_str)) } #[allow(dead_code)] diff --git a/frontend/rust-lib/flowy-ai/src/persistence/mod.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-ai/src/persistence/mod.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index d619b8ab7e..1bb05e19b6 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -5,7 +5,9 @@ use crate::entities::{ }; use crate::local_ai::controller::{LocalAIController, LocalAISetting}; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; -use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; +use flowy_ai_pub::persistence::{ + read_chat_metadata, serialize_chat_metadata, serialize_rag_ids, upsert_chat, ChatTable, +}; use std::collections::HashMap; use dashmap::DashMap; @@ -25,6 +27,7 @@ use flowy_ai_pub::cloud::ai_dto::AvailableModel; use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; +use serde_json::json; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; @@ -221,11 +224,17 @@ impl AIManager { .unwrap_or_default(); info!("[Chat] create chat with rag_ids: {:?}", rag_ids); + save_chat( + self.user_service.sqlite_connection(*uid)?, + chat_id, + "", + rag_ids.iter().map(|v| v.to_string()).collect(), + json!({}), + )?; self .cloud_service_wm - .create_chat(uid, &workspace_id, chat_id, rag_ids) + .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) .await?; - save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; let chat = Arc::new(Chat::new( self.user_service.user_id()?, @@ -705,18 +714,22 @@ async fn sync_chat_documents( Ok(()) } -fn save_chat(conn: DBConnection, chat_id: &Uuid) -> FlowyResult<()> { +fn save_chat( + conn: DBConnection, + chat_id: &Uuid, + name: &str, + rag_ids: Vec, + metadata: serde_json::Value, +) -> FlowyResult<()> { let row = ChatTable { chat_id: chat_id.to_string(), created_at: timestamp(), - name: "".to_string(), - local_files: "".to_string(), - metadata: "".to_string(), - local_enabled: false, - sync_to_cloud: false, + name: name.to_string(), + metadata: serialize_chat_metadata(&metadata), + rag_ids: Some(serialize_rag_ids(&rag_ids)), }; - insert_chat(conn, &row)?; + upsert_chat(conn, &row)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index a496e067eb..0610799f03 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -5,15 +5,15 @@ use crate::entities::{ }; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::persistence::{ - insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, - ChatMessageTable, -}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; +use flowy_ai_pub::persistence::{ + insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + ChatMessageTable, +}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; @@ -349,9 +349,9 @@ impl Chat { limit, before_message_id ); - let messages = self - .load_local_chat_messages(limit, None, before_message_id) - .await?; + + let offset = before_message_id.map_or(MessageCursor::NextBack, MessageCursor::BeforeMessageId); + let messages = self.load_local_chat_messages(limit, offset).await?; // If the number of messages equals the limit, then no need to load more messages from remote if messages.len() == limit as usize { @@ -397,9 +397,8 @@ impl Chat { limit, after_message_id, ); - let messages = self - .load_local_chat_messages(limit, after_message_id, None) - .await?; + let offset = after_message_id.map_or(MessageCursor::NextBack, MessageCursor::AfterMessageId); + let messages = self.load_local_chat_messages(limit, offset).await?; trace!( "[Chat] Loaded local chat messages: chat_id={}, messages={}", @@ -562,17 +561,10 @@ impl Chat { async fn load_local_chat_messages( &self, limit: i64, - after_message_id: Option, - before_message_id: Option, + offset: MessageCursor, ) -> Result, FlowyError> { let conn = self.user_service.sqlite_connection(self.uid)?; - let records = select_chat_messages( - conn, - &self.chat_id.to_string(), - limit, - after_message_id, - before_message_id, - )?; + let records = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?; let messages = records .into_iter() .map(|record| ChatMessagePB { diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index ccd3920e0d..400afd1507 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -12,7 +12,6 @@ pub mod local_ai; mod middleware; pub mod notification; -mod persistence; mod protobuf; mod stream_message; mod util; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 5a529d4201..a9a4022573 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -4,8 +4,8 @@ use crate::local_ai::controller::LocalAIController; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; -use crate::persistence::{select_single_message, ChatMessageTable}; use af_plugin::error::PluginError; +use flowy_ai_pub::persistence::{select_message, select_message_content, ChatMessageTable}; use std::collections::HashMap; use flowy_ai_pub::cloud::{ @@ -78,14 +78,13 @@ impl AICloudServiceMiddleware { Ok(()) } - fn get_message_record(&self, message_id: i64) -> FlowyResult { + fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; - let row = select_single_message(conn, message_id)?.ok_or_else(|| { + let content = select_message_content(conn, message_id)?.ok_or_else(|| { FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) })?; - - Ok(row) + Ok(content) } fn handle_plugin_error(&self, err: PluginError) { @@ -114,10 +113,12 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { self .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids) + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) .await } @@ -165,12 +166,12 @@ impl ChatCloudService for AICloudServiceMiddleware { info!("stream_answer use model: {:?}", ai_model); if use_local_ai { if self.local_ai.is_running() { - let row = self.get_message_record(message_id)?; + let content = self.get_message_content(message_id)?; match self .local_ai .stream_question( &chat_id.to_string(), - &row.content, + &content, Some(json!(format)), json!({}), ) @@ -202,7 +203,7 @@ impl ChatCloudService for AICloudServiceMiddleware { question_message_id: i64, ) -> Result { if self.local_ai.is_running() { - let content = self.get_message_record(question_message_id)?.content; + let content = self.get_message_content(question_message_id)?; match self .local_ai .ask_question(&chat_id.to_string(), &content) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index a84aff211c..c916157ff0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -667,11 +667,13 @@ impl ChatCloudService for ServerProvider { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { let server = self.get_server(); server? .chat_service() - .create_chat(uid, workspace_id, chat_id, rag_ids) + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) .await } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 0c7bc88859..e194f614cd 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -128,7 +128,7 @@ impl ServerProvider { let server = match server_type { Server::Local => { - let server = Arc::new(LocalServer::new(self.user.clone())); + let server = Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())); Ok::, FlowyError>(server) }, Server::AppFlowyCloud => { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 9bfd0e23fd..4b0611e497 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -35,12 +35,14 @@ where workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatParams { chat_id, - name: "".to_string(), + name: name.to_string(), rag_ids, }; try_get_client? diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index fe90f45e77..23562d2b07 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,14 +1,22 @@ use crate::af_cloud::define::ServerUser; use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use flowy_ai::local_ai::controller::LocalAIController; +use flowy_ai::local_ai::stream_util::QuestionStream; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; -use flowy_error::FlowyError; +use flowy_ai_pub::persistence::{ + deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_message_content, + serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, ChatTable, + ChatTableChangeset, +}; +use flowy_error::{FlowyError, FlowyResult}; +use futures_util::{stream, FutureExt, StreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; -use serde_json::Value; +use serde_json::{json, Value}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; @@ -16,6 +24,18 @@ use uuid::Uuid; pub struct LocalServerChatServiceImpl { pub user: Arc, + pub local_ai: Arc, +} + +impl LocalServerChatServiceImpl { + fn get_message_content(&self, message_id: i64) -> FlowyResult { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let content = select_message_content(db, message_id)?.ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) + })?; + Ok(content) + } } #[async_trait] @@ -24,9 +44,28 @@ impl ChatCloudService for LocalServerChatServiceImpl { &self, _uid: &i64, _workspace_id: &Uuid, - _chat_id: &Uuid, - _rag_ids: Vec, + chat_id: &Uuid, + rag_ids: Vec, + name: &str, + metadata: Value, ) -> Result<(), FlowyError> { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + + let rag_ids = rag_ids + .iter() + .map(|v| v.to_string()) + .collect::>(); + + let row = ChatTable { + chat_id: chat_id.to_string(), + created_at: timestamp(), + name: name.to_string(), + metadata: serialize_chat_metadata(&metadata), + rag_ids: Some(serialize_rag_ids(&rag_ids)), + }; + + upsert_chat(db, &row)?; Ok(()) } @@ -66,21 +105,52 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn stream_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _message_id: i64, - _format: ResponseFormat, + chat_id: &Uuid, + message_id: i64, + format: ResponseFormat, _ai_model: Option, ) -> Result { + if self.local_ai.is_running() { + let content = self.get_message_content(message_id)?; + match self + .local_ai + .stream_question( + &chat_id.to_string(), + &content, + Some(json!(format)), + json!({}), + ) + .await + { + Ok(stream) => Ok(QuestionStream::new(stream).boxed()), + Err(err) => Ok( + stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), + ), + } + } else { + Err(FlowyError::local_ai_not_ready()) + } + } + + async fn get_answer( + &self, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _question_message_id: i64, + ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_chat_messages( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _offset: MessageCursor, - _limit: u64, + chat_id: &Uuid, + offset: MessageCursor, + limit: u64, ) -> Result { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } @@ -106,15 +176,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { }) } - async fn get_answer( - &self, - _workspace_id: &Uuid, - _chat_id: &Uuid, - _question_message_id: i64, - ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) - } - async fn stream_complete( &self, _workspace_id: &Uuid, @@ -137,18 +198,40 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn get_chat_settings( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let chat_id = chat_id.to_string(); + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let row = read_chat(db, &chat_id)?; + let rag_ids = deserialize_rag_ids(&row.rag_ids); + let metadata = deserialize_chat_metadata::(&row.metadata); + let setting = ChatSettings { + name: row.name, + rag_ids, + metadata, + }; + + Ok(setting) } async fn update_chat_settings( &self, _workspace_id: &Uuid, - _id: &Uuid, - _s: UpdateChatParams, + id: &Uuid, + s: UpdateChatParams, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let uid = self.user.user_id()?; + let mut db = self.user.get_sqlite_db(uid)?; + let changeset = ChatTableChangeset { + chat_id: id.to_string(), + name: s.name, + metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), + rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), + }; + + update_chat(&mut db, changeset)?; + Ok(()) } async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 282d118203..0ef930320e 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -8,6 +8,7 @@ use crate::local_server::impls::{ LocalServerUserServiceImpl, }; use crate::AppFlowyServer; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; @@ -18,13 +19,15 @@ use tokio::sync::mpsc; pub struct LocalServer { user: Arc, + local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, + local_ai, stop_tx: Default::default(), } } @@ -61,6 +64,7 @@ impl AppFlowyServer for LocalServer { fn chat_service(&self) -> Arc { Arc::new(LocalServerChatServiceImpl { user: self.user.clone(), + local_ai: self.local_ai.clone(), }) } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql new file mode 100644 index 0000000000..6edd4a5113 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql @@ -0,0 +1,9 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE chat_table + ADD COLUMN local_enabled INTEGER; +ALTER TABLE chat_table + ADD COLUMN sync_to_cloud INTEGER; +ALTER TABLE chat_table + ADD COLUMN local_files TEXT; + +ALTER TABLE chat_table DROP COLUMN rag_ids; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql new file mode 100644 index 0000000000..0604601486 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE chat_table DROP COLUMN local_enabled; +ALTER TABLE chat_table DROP COLUMN local_files; +ALTER TABLE chat_table DROP COLUMN sync_to_cloud; +ALTER TABLE chat_table ADD COLUMN rag_ids TEXT; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 4ff70bf3c6..eda95c29f7 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -35,10 +35,8 @@ diesel::table! { chat_id -> Text, created_at -> BigInt, name -> Text, - local_files -> Text, metadata -> Text, - local_enabled -> Bool, - sync_to_cloud -> Bool, + rag_ids -> Nullable, } } @@ -128,15 +126,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 13065ac726e8e52ab9f0ff39838079b9e29eb926 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 15:47:17 +0800 Subject: [PATCH 343/384] chore: add tests --- frontend/rust-lib/Cargo.lock | 169 +---- .../event-integration-test/Cargo.toml | 9 - .../event-integration-test/tests/main.rs | 2 + .../tests/sql_test/chat_message_test.rs | 596 ++++++++++++++++++ .../tests/sql_test/mod.rs | 1 + .../src/persistence/chat_message_sql.rs | 48 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 6 +- frontend/rust-lib/flowy-ai/src/chat.rs | 32 +- .../rust-lib/flowy-ai/src/event_handler.rs | 4 +- .../flowy-ai/src/local_ai/controller.rs | 2 - .../src/middleware/chat_service_mw.rs | 4 +- .../src/deps_resolve/cloud_service_impl.rs | 2 +- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../flowy-server/src/af_cloud/impls/chat.rs | 2 +- .../src/local_server/impls/chat.rs | 151 ++++- .../flowy-server/tests/af_cloud_test/util.rs | 2 +- .../2025-04-17-042326_chat_metadata/down.sql | 2 +- 17 files changed, 795 insertions(+), 238 deletions(-) create mode 100644 frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs create mode 100644 frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 29547be823..cd06dc4870 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2313,7 +2313,6 @@ dependencies = [ name = "event-integration-test" version = "0.1.0" dependencies = [ - "anyhow", "assert-json-diff", "bytes", "chrono", @@ -2322,15 +2321,11 @@ dependencies = [ "collab-document", "collab-entity", "collab-folder", - "collab-plugins", - "dotenv", "flowy-ai", "flowy-ai-pub", "flowy-core", - "flowy-database-pub", "flowy-database2", "flowy-document", - "flowy-document-pub", "flowy-folder", "flowy-folder-pub", "flowy-notification", @@ -2342,7 +2337,6 @@ dependencies = [ "flowy-user", "flowy-user-pub", "futures", - "futures-util", "lib-dispatch", "lib-infra", "nanoid", @@ -2352,10 +2346,7 @@ dependencies = [ "serde", "serde_json", "strum", - "tempdir", - "thread-id", "tokio", - "tokio-postgres", "tracing", "uuid", "walkdir", @@ -2415,12 +2406,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fancy-regex" version = "0.10.0" @@ -2491,12 +2476,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -2959,6 +2938,7 @@ dependencies = [ "arc-swap", "assert-json-diff", "bytes", + "chrono", "client-api", "collab", "collab-database", @@ -3199,12 +3179,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "funty" version = "2.0.0" @@ -4621,15 +4595,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "md-5" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" -dependencies = [ - "digest", -] - [[package]] name = "md5" version = "0.7.0" @@ -5347,35 +5312,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "postgres-protocol" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" -dependencies = [ - "base64 0.21.5", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.8.5", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" -dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -5796,19 +5732,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.7.3" @@ -5854,21 +5777,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -5944,15 +5852,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -6053,15 +5952,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "rend" version = "0.4.0" @@ -6948,17 +6838,6 @@ dependencies = [ "quote", ] -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "strsim" version = "0.10.0" @@ -7296,16 +7175,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - [[package]] name = "tempfile" version = "3.12.0" @@ -7538,32 +7407,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.1", - "percent-encoding", - "phf 0.11.2", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.8.5", - "socket2 0.5.5", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-retry" version = "0.3.0" @@ -8448,16 +8291,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 21d94ae28a..6b2d5af7ba 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -12,16 +12,13 @@ flowy-user-pub = { workspace = true } flowy-folder = { path = "../flowy-folder", features = ["test_helper"] } flowy-folder-pub = { workspace = true } flowy-database2 = { path = "../flowy-database2" } -flowy-database-pub = { workspace = true } flowy-document = { path = "../flowy-document" } -flowy-document-pub = { workspace = true } flowy-ai = { workspace = true } lib-dispatch = { workspace = true } lib-infra = { workspace = true } flowy-server = { path = "../flowy-server" } flowy-server-pub = { workspace = true } flowy-notification = { workspace = true } -anyhow.workspace = true flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-search = { workspace = true } @@ -31,8 +28,6 @@ serde.workspace = true serde_json.workspace = true protobuf.workspace = true tokio = { workspace = true, features = ["full"] } -futures-util = "0.3.26" -thread-id = "3.3.0" bytes.workspace = true nanoid = "0.4.0" tracing.workspace = true @@ -41,17 +36,13 @@ collab = { workspace = true } collab-document = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } -collab-plugins = { workspace = true } collab-entity = { workspace = true } rand = { version = "0.8.5", features = [] } strum = "0.25.0" [dev-dependencies] -dotenv = "0.15.0" -tempdir = "0.3.7" uuid.workspace = true assert-json-diff = "2.0.2" -tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" zip.workspace = true walkdir = "2.5.0" diff --git a/frontend/rust-lib/event-integration-test/tests/main.rs b/frontend/rust-lib/event-integration-test/tests/main.rs index 05f19e9b75..cf4c1591ac 100644 --- a/frontend/rust-lib/event-integration-test/tests/main.rs +++ b/frontend/rust-lib/event-integration-test/tests/main.rs @@ -4,6 +4,8 @@ mod folder; // TODO(Mathias): Enable tests for search // mod search; + +mod sql_test; mod user; pub mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs new file mode 100644 index 0000000000..549548ccf1 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs @@ -0,0 +1,596 @@ +use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_ai_pub::cloud::MessageCursor; +use flowy_ai_pub::persistence::{ + insert_chat_messages, select_chat_messages, select_message, select_message_content, + select_message_where_match_reply_message_id, total_message_count, ChatMessageTable, +}; +use uuid::Uuid; + +#[tokio::test] +async fn chat_message_table_insert_select_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id_1 = 1000; + let message_id_2 = 2000; + + // Create test messages + let messages = vec![ + ChatMessageTable { + message_id: message_id_1, + chat_id: chat_id.clone(), + content: "Hello, this is a test message".to_string(), + created_at: 1625097600, // 2021-07-01 + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }, + ChatMessageTable { + message_id: message_id_2, + chat_id: chat_id.clone(), + content: "This is a reply to the test message".to_string(), + created_at: 1625097700, // 2021-07-01, 100 seconds later + author_type: 0, // AI + author_id: "ai".to_string(), + reply_message_id: Some(message_id_1), + metadata: Some(r#"{"source": "test"}"#.to_string()), + }, + ]; + + // Test insert_chat_messages + let result = insert_chat_messages(db_conn, &messages); + assert!( + result.is_ok(), + "Failed to insert chat messages: {:?}", + result + ); + + // Test select_chat_messages + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let messages_result = + select_chat_messages(db_conn, &chat_id, 10, MessageCursor::Offset(0)).unwrap(); + + assert_eq!(messages_result.messages.len(), 2); + assert_eq!(messages_result.total_count, 2); + assert!(!messages_result.has_more); + + // Verify the content of the returned messages + let first_message = messages_result + .messages + .iter() + .find(|m| m.message_id == message_id_1) + .unwrap(); + assert_eq!(first_message.content, "Hello, this is a test message"); + assert_eq!(first_message.author_type, 1); + + let second_message = messages_result + .messages + .iter() + .find(|m| m.message_id == message_id_2) + .unwrap(); + assert_eq!( + second_message.content, + "This is a reply to the test message" + ); + assert_eq!(second_message.reply_message_id, Some(message_id_1)); +} + +#[tokio::test] +async fn chat_message_table_cursor_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create multiple test messages with sequential IDs + let mut messages = Vec::new(); + for i in 1..6 { + messages.push(ChatMessageTable { + message_id: i * 1000, + chat_id: chat_id.clone(), + content: format!("Message {}", i), + created_at: 1625097600 + (i * 100), // Increasing timestamps + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }); + } + + // Insert messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Test MessageCursor::Offset + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_offset = select_chat_messages( + db_conn, + &chat_id, + 2, // Limit to 2 messages + MessageCursor::Offset(0), + ) + .unwrap(); + + assert_eq!(result_offset.messages.len(), 2); + assert!(result_offset.has_more); + + // Test MessageCursor::AfterMessageId + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_after = select_chat_messages( + db_conn, + &chat_id, + 3, // Limit to 3 messages + MessageCursor::AfterMessageId(2000), + ) + .unwrap(); + + assert_eq!(result_after.messages.len(), 3); // Should get message IDs 3000, 4000, 5000 + assert!(result_after.messages.iter().all(|m| m.message_id > 2000)); + + // Test MessageCursor::BeforeMessageId + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_before = select_chat_messages( + db_conn, + &chat_id, + 2, // Limit to 2 messages + MessageCursor::BeforeMessageId(4000), + ) + .unwrap(); + + assert_eq!(result_before.messages.len(), 2); // Should get message IDs 1000, 2000, 3000 + assert!(result_before.messages.iter().all(|m| m.message_id < 4000)); +} + +#[tokio::test] +async fn chat_message_total_count_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create test messages + let messages = vec![ + ChatMessageTable { + message_id: 1001, + chat_id: chat_id.clone(), + content: "Message 1".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }, + ChatMessageTable { + message_id: 1002, + chat_id: chat_id.clone(), + content: "Message 2".to_string(), + created_at: 1625097700, + author_type: 0, + author_id: "ai".to_string(), + reply_message_id: None, + metadata: None, + }, + ]; + + // Insert messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Test total_message_count + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 2); + + // Add one more message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let additional_message = ChatMessageTable { + message_id: 1003, + chat_id: chat_id.clone(), + content: "Message 3".to_string(), + created_at: 1625097800, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + insert_chat_messages(db_conn, &[additional_message]).unwrap(); + + // Verify count increased + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let updated_count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(updated_count, 3); + + // Test count for non-existent chat + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let empty_count = total_message_count(db_conn, "non_existent_chat").unwrap(); + assert_eq!(empty_count, 0); +} + +#[tokio::test] +async fn chat_message_select_message_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 2001; + + // Create test message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: "This is a test message for select_message".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Test select_message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message(db_conn, message_id).unwrap(); + assert!(result.is_some()); + + let retrieved_message = result.unwrap(); + assert_eq!(retrieved_message.message_id, message_id); + assert_eq!(retrieved_message.chat_id, chat_id); + assert_eq!( + retrieved_message.content, + "This is a test message for select_message" + ); + assert_eq!(retrieved_message.author_id, "user_1"); + assert_eq!( + retrieved_message.metadata, + Some(r#"{"test_key": "test_value"}"#.to_string()) + ); + + // Test select_message with non-existent ID + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let non_existent = select_message(db_conn, 9999).unwrap(); + assert!(non_existent.is_none()); +} + +#[tokio::test] +async fn chat_message_select_content_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 3001; + let message_content = "This is the content to retrieve"; + + // Create test message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: message_content.to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Test select_message_content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let content = select_message_content(db_conn, message_id).unwrap(); + assert!(content.is_some()); + assert_eq!(content.unwrap(), message_content); + + // Test with non-existent message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let no_content = select_message_content(db_conn, 9999).unwrap(); + assert!(no_content.is_none()); +} + +#[tokio::test] +async fn chat_message_reply_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let question_id = 4001; + let answer_id = 4002; + + // Create question and answer messages + let question = ChatMessageTable { + message_id: question_id, + chat_id: chat_id.clone(), + content: "What is the question?".to_string(), + created_at: 1625097600, + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + let answer = ChatMessageTable { + message_id: answer_id, + chat_id: chat_id.clone(), + content: "This is the answer".to_string(), + created_at: 1625097700, + author_type: 0, // AI + author_id: "ai".to_string(), + reply_message_id: Some(question_id), // Link to question + metadata: None, + }; + + // Insert messages + insert_chat_messages(db_conn, &[question, answer]).unwrap(); + + // Test select_message_where_match_reply_message_id + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); + + assert!(result.is_some()); + let reply = result.unwrap(); + assert_eq!(reply.message_id, answer_id); + assert_eq!(reply.content, "This is the answer"); + assert_eq!(reply.reply_message_id, Some(question_id)); + + // Test with non-existent reply relation + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let no_reply = select_message_where_match_reply_message_id( + db_conn, &chat_id, 9999, // Non-existent question ID + ) + .unwrap(); + + assert!(no_reply.is_none()); + + // Test with wrong chat_id + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let wrong_chat = + select_message_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); + + assert!(wrong_chat.is_none()); +} + +#[tokio::test] +async fn chat_message_upsert_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 5001; + + // Create initial message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: "Original content".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Check original content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let original = select_message(db_conn, message_id).unwrap().unwrap(); + assert_eq!(original.content, "Original content"); + + // Create updated message with same ID but different content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let updated_message = ChatMessageTable { + message_id, // Same ID + chat_id: chat_id.clone(), + content: "Updated content".to_string(), // New content + created_at: 1625097700, // Updated timestamp + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: Some(1000), // Added reply ID + metadata: Some(r#"{"updated": true}"#.to_string()), + }; + + // Upsert message + insert_chat_messages(db_conn, &[updated_message]).unwrap(); + + // Verify update + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message(db_conn, message_id).unwrap().unwrap(); + assert_eq!(result.content, "Updated content"); + assert_eq!(result.created_at, 1625097700); + assert_eq!(result.reply_message_id, Some(1000)); + assert_eq!(result.metadata, Some(r#"{"updated": true}"#.to_string())); + + // Count should still be 1 (update, not insert) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 1); +} + +#[tokio::test] +async fn chat_message_select_with_large_dataset() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create 100 test messages with sequential IDs + let mut messages = Vec::new(); + for i in 1..=100 { + messages.push(ChatMessageTable { + message_id: i * 100, + chat_id: chat_id.clone(), + content: format!("Message {}", i), + created_at: 1625097600 + (i * 10), // Increasing timestamps + author_type: if i % 2 == 0 { 0 } else { 1 }, // Alternate between AI and User + author_id: if i % 2 == 0 { + "ai".to_string() + } else { + "user_1".to_string() + }, + reply_message_id: if i > 1 && i % 2 == 0 { + Some((i - 1) * 100) + } else { + None + }, // Even messages reply to previous message + metadata: if i % 5 == 0 { + Some(format!(r#"{{"index": {}}}"#, i)) + } else { + None + }, + }); + } + + // Insert all 100 messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Verify total count + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 100, "Should have 100 messages in the database"); + + // Test 1: MessageCursor::Offset with small page size + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let page_size = 10; + let result_offset = + select_chat_messages(db_conn, &chat_id, page_size, MessageCursor::Offset(0)).unwrap(); + + assert_eq!( + result_offset.messages.len(), + page_size as usize, + "Should return exactly {page_size} messages" + ); + assert!( + result_offset.has_more, + "Should have more messages available" + ); + assert_eq!(result_offset.total_count, 100, "Total count should be 100"); + + // Test 2: Pagination with offset + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_page2 = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::Offset(page_size), + ) + .unwrap(); + + assert_eq!(result_page2.messages.len(), page_size as usize); + assert!( + result_page2.messages[0].message_id != result_offset.messages[0].message_id, + "Second page should have different messages than first page" + ); + + // Test 3: MessageCursor::AfterMessageId (forward pagination) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let middle_message_id = 5000; // Message ID from the middle + let result_after = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::AfterMessageId(middle_message_id), + ) + .unwrap(); + + assert_eq!(result_after.messages.len(), page_size as usize); + assert!( + result_after + .messages + .iter() + .all(|m| m.message_id > middle_message_id), + "All messages should have ID greater than the cursor" + ); + + // Test 4: MessageCursor::BeforeMessageId (backward pagination) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_before = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::BeforeMessageId(middle_message_id), + ) + .unwrap(); + + assert_eq!(result_before.messages.len(), page_size as usize); + assert!( + result_before + .messages + .iter() + .all(|m| m.message_id < middle_message_id), + "All messages should have ID less than the cursor" + ); + + // Test 5: Large page size (retrieve all) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_all = select_chat_messages( + db_conn, + &chat_id, + 200, // More than we have + MessageCursor::Offset(0), + ) + .unwrap(); + + assert_eq!( + result_all.messages.len(), + 100, + "Should return all 100 messages" + ); + assert!(!result_all.has_more, "Should not have more messages"); + + // Test 6: Empty result when using out of range cursor + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_out_of_range = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::AfterMessageId(10000), // After the last message + ) + .unwrap(); + + assert_eq!( + result_out_of_range.messages.len(), + 0, + "Should return no messages" + ); + assert!( + !result_out_of_range.has_more, + "Should not have more messages" + ); +} diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs new file mode 100644 index 0000000000..773bdab81f --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs @@ -0,0 +1 @@ +mod chat_message_test; diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index ac3192064b..f5f985575a 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -35,6 +35,7 @@ pub fn insert_chat_messages( .do_update() .set(( chat_message_table::content.eq(excluded(chat_message_table::content)), + chat_message_table::metadata.eq(excluded(chat_message_table::metadata)), chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), @@ -48,12 +49,18 @@ pub fn insert_chat_messages( Ok(()) } +pub struct ChatMessagesResult { + pub messages: Vec, + pub total_count: i64, + pub has_more: bool, +} + pub fn select_chat_messages( mut conn: DBConnection, chat_id_val: &str, - limit_val: i64, + limit_val: u64, offset: MessageCursor, -) -> QueryResult> { +) -> QueryResult { let mut query = dsl::chat_message_table .filter(chat_message_table::chat_id.eq(chat_id_val)) .into_boxed(); @@ -71,15 +78,46 @@ pub fn select_chat_messages( MessageCursor::NextBack => {}, } + // Get total count before applying limit + let total_count = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .count() + .first::(&mut *conn)?; + query = query .order(( chat_message_table::created_at.desc(), chat_message_table::message_id.desc(), )) - .limit(limit_val); + .limit(limit_val as i64); let messages: Vec = query.load::(&mut *conn)?; - Ok(messages) + + // Check if there are more messages + let has_more = if let Some(last_message) = messages.last() { + let remaining_count = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .filter(chat_message_table::message_id.lt(last_message.message_id)) + .count() + .first::(&mut *conn)?; + + remaining_count > 0 + } else { + false + }; + + Ok(ChatMessagesResult { + messages, + total_count, + has_more, + }) +} + +pub fn total_message_count(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { + dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .count() + .first::(&mut *conn) } pub fn select_message( @@ -107,10 +145,12 @@ pub fn select_message_content( pub fn select_message_where_match_reply_message_id( mut conn: DBConnection, + chat_id: &str, answer_message_id_val: i64, ) -> QueryResult> { dsl::chat_message_table .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) + .filter(chat_message_table::chat_id.eq(chat_id)) .first::(&mut *conn) .optional() } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 1bb05e19b6..a49771a544 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -270,7 +270,7 @@ impl AIManager { ) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; let question_message_id = chat - .get_question_id_from_answer_id(answer_message_id) + .get_question_id_from_answer_id(chat_id, answer_message_id) .await?; let model = model.map_or_else( @@ -567,7 +567,7 @@ impl AIManager { pub async fn load_prev_chat_messages( &self, chat_id: &Uuid, - limit: i64, + limit: u64, before_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -580,7 +580,7 @@ impl AIManager { pub async fn load_latest_chat_messages( &self, chat_id: &Uuid, - limit: i64, + limit: u64, after_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 0610799f03..976acee3e4 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -63,18 +63,6 @@ impl Chat { pub fn close(&self) {} - #[allow(dead_code)] - pub async fn pull_latest_message(&self, limit: i64) { - let latest_message_id = self - .latest_message_id - .load(std::sync::atomic::Ordering::Relaxed); - if latest_message_id > 0 { - let _ = self - .load_remote_chat_messages(limit, None, Some(latest_message_id)) - .await; - } - } - pub async fn stop_stream_message(&self) { self .stop_stream @@ -340,7 +328,7 @@ impl Chat { /// - `before_message_id` is the first message ID in the current chat messages. pub async fn load_prev_chat_messages( &self, - limit: i64, + limit: u64, before_message_id: Option, ) -> Result { trace!( @@ -388,7 +376,7 @@ impl Chat { pub async fn load_latest_chat_messages( &self, - limit: i64, + limit: u64, after_message_id: Option, ) -> Result { trace!( @@ -420,7 +408,7 @@ impl Chat { async fn load_remote_chat_messages( &self, - limit: i64, + limit: u64, before_message_id: Option, after_message_id: Option, ) -> FlowyResult<()> { @@ -445,7 +433,7 @@ impl Chat { _ => MessageCursor::NextBack, }; match cloud_service - .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit as u64) + .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit) .await { Ok(resp) => { @@ -498,12 +486,14 @@ impl Chat { pub async fn get_question_id_from_answer_id( &self, + chat_id: &Uuid, answer_message_id: i64, ) -> Result { let conn = self.user_service.sqlite_connection(self.uid)?; - let local_result = select_message_where_match_reply_message_id(conn, answer_message_id)? - .map(|message| message.message_id); + let local_result = + select_message_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? + .map(|message| message.message_id); if let Some(message_id) = local_result { return Ok(message_id); @@ -560,12 +550,12 @@ impl Chat { async fn load_local_chat_messages( &self, - limit: i64, + limit: u64, offset: MessageCursor, ) -> Result, FlowyError> { let conn = self.user_service.sqlite_connection(self.uid)?; - let records = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?; - let messages = records + let rows = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?.messages; + let messages = rows .into_iter() .map(|record| ChatMessagePB { message_id: record.message_id, diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index b8334ffe8d..6788685102 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -152,7 +152,7 @@ pub(crate) async fn load_prev_message_handler( let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_prev_chat_messages(&chat_id, data.limit, data.before_message_id) + .load_prev_chat_messages(&chat_id, data.limit as u64, data.before_message_id) .await?; data_result_ok(messages) } @@ -168,7 +168,7 @@ pub(crate) async fn load_next_message_handler( let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_latest_chat_messages(&chat_id, data.limit, data.after_message_id) + .load_latest_chat_messages(&chat_id, data.limit as u64, data.after_message_id) .await?; data_result_ok(messages) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 431d8ff681..3c4ccb1f9d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -79,7 +79,6 @@ impl LocalAIController { // Create the core plugin and resource controller let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { - user_service: user_service.clone(), store_preferences: store_preferences.clone(), }; let local_ai_resource = Arc::new(LocalAIResourceController::new( @@ -594,7 +593,6 @@ async fn initialize_ai_plugin( } pub struct LLMResourceServiceImpl { - user_service: Arc, store_preferences: Weak, } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index a9a4022573..e3c3d671ec 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -5,14 +5,14 @@ use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use af_plugin::error::PluginError; -use flowy_ai_pub::persistence::{select_message, select_message_content, ChatMessageTable}; +use flowy_ai_pub::persistence::select_message_content; use std::collections::HashMap; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + StreamComplete, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index c916157ff0..e4bffbfb69 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -16,7 +16,7 @@ use flowy_ai_pub::cloud::search_dto::{ use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + StreamComplete, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 5225eb817d..c8710470b0 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -45,6 +45,7 @@ rand = "0.8.5" semver = "1.0.23" flowy-sqlite = { workspace = true } flowy-ai = { workspace = true } +chrono.workspace = true [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 4b0611e497..3f8803c7fc 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -9,7 +9,7 @@ use client_api::entity::chat_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ModelList, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 23562d2b07..09ef644bfc 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,25 +1,29 @@ use crate::af_cloud::define::ServerUser; +use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::CompletionStream; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai::local_ai::stream_util::QuestionStream; +use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, + ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, + RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_message_content, - serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, ChatTable, - ChatTableChangeset, + deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_chat_messages, + select_message_content, select_message_where_match_reply_message_id, serialize_chat_metadata, + serialize_rag_ids, update_chat, upsert_chat, ChatMessageTable, ChatTable, ChatTableChangeset, }; use flowy_error::{FlowyError, FlowyResult}; -use futures_util::{stream, FutureExt, StreamExt}; +use futures_util::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; use serde_json::{json, Value}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; +use tracing::trace; use uuid::Uuid; pub struct LocalServerChatServiceImpl { @@ -148,51 +152,119 @@ impl ChatCloudService for LocalServerChatServiceImpl { offset: MessageCursor, limit: u64, ) -> Result { + let chat_id = chat_id.to_string(); let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; + let result = select_chat_messages(db, &chat_id, limit, offset)?; - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let messages = result + .messages + .into_iter() + .map(chat_message_from_row) + .collect(); + + Ok(RepeatedChatMessage { + messages, + has_more: result.has_more, + total: result.total_count, + }) } async fn get_question_from_answer_id( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _answer_message_id: i64, + chat_id: &Uuid, + answer_message_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let chat_id = chat_id.to_string(); + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let row = select_message_where_match_reply_message_id(db, &chat_id, answer_message_id)? + .map(chat_message_from_row) + .ok_or_else(FlowyError::record_not_found)?; + Ok(row) } async fn get_related_message( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message_id: i64, - ai_model: Option, + _ai_model: Option, ) -> Result { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], - }) + if self.local_ai.is_running() { + let questions = self + .local_ai + .get_related_question(&chat_id.to_string()) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + trace!("LocalAI related questions: {:?}", questions); + + let items = questions + .into_iter() + .map(|content| RelatedQuestion { + content, + metadata: None, + }) + .collect::>(); + + Ok(RepeatedRelatedQuestion { message_id, items }) + } else { + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], + }) + } } async fn stream_complete( &self, _workspace_id: &Uuid, - _params: CompleteTextParams, + params: CompleteTextParams, _ai_model: Option, ) -> Result { - Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) + if self.local_ai.is_running() { + match self + .local_ai + .complete_text_v2( + ¶ms.text, + params.completion_type.unwrap() as u8, + Some(json!(params.format)), + Some(json!(params.metadata)), + ) + .await + { + Ok(stream) => Ok( + CompletionStream::new( + stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), + ) + .map_err(FlowyError::from) + .boxed(), + ), + Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), + } + } else { + Err(FlowyError::local_ai_not_ready()) + } } async fn embed_file( &self, _workspace_id: &Uuid, - _file_path: &Path, - _chat_id: &Uuid, - _metadata: Option>, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) + if self.local_ai.is_running() { + self + .local_ai + .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + Ok(()) + } else { + Err(FlowyError::local_ai_not_ready()) + } } async fn get_chat_settings( @@ -242,3 +314,36 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } } + +fn chat_message_from_row(row: ChatMessageTable) -> ChatMessage { + let created_at = Utc + .timestamp_opt(row.created_at, 0) + .single() + .unwrap_or_else(Utc::now); + + let author_id = row.author_id.parse::().unwrap_or_default(); + let author_type = match row.author_type { + 1 => ChatAuthorType::Human, + 2 => ChatAuthorType::System, + 3 => ChatAuthorType::AI, + _ => ChatAuthorType::Unknown, + }; + + let metadata = row + .metadata + .map(|s| deserialize_chat_metadata::(&s)) + .unwrap_or_else(|| json!({})); + + ChatMessage { + author: ChatAuthor { + author_id, + author_type, + meta: None, + }, + message_id: row.message_id, + content: row.content, + created_at, + metadata, + reply_message_id: row.reply_message_id, + } +} diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index b747c0b2ae..3004ce2163 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -48,7 +48,7 @@ impl ServerUser for FakeServerUserImpl { todo!() } - fn get_sqlite_db(&self, uid: i64) -> Result { + fn get_sqlite_db(&self, _uid: i64) -> Result { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql index 6edd4a5113..8b07e6189d 100644 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql @@ -6,4 +6,4 @@ ALTER TABLE chat_table ALTER TABLE chat_table ADD COLUMN local_files TEXT; -ALTER TABLE chat_table DROP COLUMN rag_ids; +ALTER TABLE chat_table DROP COLUMN rag_ids; \ No newline at end of file From 57e4d269eb097a527b5982fee2a7a5fd13bea0e4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 16:27:53 +0800 Subject: [PATCH 344/384] chore: enable local chat --- .../lib/plugins/ai_chat/chat_page.dart | 16 ++++++------- .../setting_ai_view/settings_ai_view.dart | 17 ------------- .../settings/settings_dialog.dart | 7 +++++- frontend/rust-lib/Cargo.lock | 24 +++++++++---------- frontend/rust-lib/Cargo.toml | 4 ++-- 5 files changed, 28 insertions(+), 40 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 4f843d447b..52617f1292 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -51,14 +51,14 @@ class AIChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { - return Center( - child: FlowyText( - LocaleKeys.chat_unsupportedCloudPrompt.tr(), - fontSize: 20, - ), - ); - } + // if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + // return Center( + // child: FlowyText( + // LocaleKeys.chat_unsupportedCloudPrompt.tr(), + // fontSize: 20, + // ), + // ); + // } return MultiBlocProvider( providers: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index efb969700e..c2e75ff2f2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -10,23 +10,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { - const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), - child: FlowyText( - LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), - maxLines: null, - fontSize: 16, - lineHeight: 1.6, - ), - ); - } -} - class SettingsAIView extends StatelessWidget { const SettingsAIView({ super.key, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 08747b95da..512d673407 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -32,6 +32,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'pages/setting_ai_view/local_settings_ai_view.dart'; import 'widgets/setting_cloud.dart'; @visibleForTesting @@ -147,7 +148,11 @@ class SettingsDialog extends StatelessWidget { workspaceId: workspaceId, ); } else { - return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); + return LocalSettingsAIView( + key: ValueKey(workspaceId), + userProfile: user, + workspaceId: workspaceId, + ); } case SettingsPage.member: return WorkspaceMembersPage( diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index cd06dc4870..ec114f205a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "async-trait", @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "bincode", "bytes", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3442,7 +3442,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "app-error", "jsonwebtoken", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bytes", @@ -6644,7 +6644,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6e93b13a19..e4f0eab545 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -107,8 +107,8 @@ af-local-ai = { version = "0.1" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } [profile.dev] opt-level = 0 From 3a4d17f054687c41854d2fcfe94b58c9b9f1f83e Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 16:53:52 +0800 Subject: [PATCH 345/384] chore: enable anon ai writer --- .../ai/ai_writer_toolbar_item.dart | 14 ++++---- .../local_settings_ai_view.dart | 34 +++++++++++++++++++ .../src/local_server/impls/chat.rs | 12 +++++-- 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 467a847c53..a8071e9286 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -142,7 +142,7 @@ class _AiWriterToolbarActionListState extends State { ], ), onPressed: () { - if (_isAIEnabled(widget.editorState)) { + if (_isAIWriterEnabled(widget.editorState)) { keepEditorFocusNotifier.increase(); popoverController.show(); setState(() { @@ -159,7 +159,7 @@ class _AiWriterToolbarActionListState extends State { return widget.tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIEnabled(widget.editorState) + _isAIWriterEnabled(widget.editorState) ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -191,7 +191,7 @@ class ImproveWritingButton extends StatelessWidget { color: theme.iconColorTheme.primary, ), onPressed: () { - if (_isAIEnabled(editorState)) { + if (_isAIWriterEnabled(editorState)) { keepEditorFocusNotifier.increase(); _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { @@ -205,7 +205,7 @@ class ImproveWritingButton extends StatelessWidget { return tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIEnabled(editorState) + _isAIWriterEnabled(editorState) ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -240,10 +240,8 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async { ); } -bool _isAIEnabled(EditorState editorState) { - final documentContext = editorState.document.root.context; - return documentContext == null || - !documentContext.read().isLocalMode; +bool _isAIWriterEnabled(EditorState editorState) { + return true; } bool onlyShowInTextTypeAndExcludeTable( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart new file mode 100644 index 0000000000..e90c42444f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalSettingsAIView extends StatelessWidget { + const LocalSettingsAIView({ + super.key, + required this.userProfile, + required this.workspaceId, + }); + + final UserProfilePB userProfile; + final String workspaceId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => SettingsAIBloc(userProfile, workspaceId) + ..add(const SettingsAIEvent.started()), + child: SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: "", + children: [ + const LocalAISetting(), + ], + ), + ); + } +} diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 09ef644bfc..9057288f88 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -132,7 +132,11 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } @@ -244,7 +248,11 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } From ac659066c674bd1f95d3e637dac040767d5d3951 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 20:49:24 +0800 Subject: [PATCH 346/384] chore: return local model --- .../lib/plugins/ai_chat/chat_page.dart | 3 - .../ai/ai_writer_toolbar_item.dart | 2 - frontend/rust-lib/flowy-ai/src/ai_manager.rs | 178 ++++++++++-------- .../flowy-core/src/deps_resolve/chat_deps.rs | 7 +- frontend/rust-lib/flowy-core/src/lib.rs | 7 + .../rust-lib/flowy-core/src/server_layer.rs | 6 + .../flowy-server/src/af_cloud/define.rs | 3 + .../flowy-server/tests/af_cloud_test/util.rs | 6 + .../src/services/authenticate_user.rs | 24 +++ .../src/user_manager/manager_history_user.rs | 2 +- 10 files changed, 150 insertions(+), 88 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 52617f1292..c154b0e3a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -9,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index a8071e9286..ab695b31a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -9,7 +8,6 @@ import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'operations/ai_writer_entities.dart'; diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a49771a544..c2f5b223c0 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -35,8 +35,10 @@ use tokio::sync::RwLock; use tracing::{error, info, instrument, trace}; use uuid::Uuid; +#[async_trait] pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; + async fn is_local_model(&self) -> FlowyResult; fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; @@ -437,99 +439,113 @@ impl AIManager { } pub async fn get_available_models(&self, source: String) -> FlowyResult { - // Build the models list from server models and mark them as non-local. - let mut models: Vec = self - .get_server_available_models() - .await? - .into_iter() - .map(AIModel::from) - .collect(); + let is_local_mode = self.user_service.is_local_model().await?; + if is_local_mode { + let mut selected_model = AIModel::default(); + let mut models = vec![]; + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + let model = AIModel::local(local_model, "".to_string()); + selected_model = model.clone(); + models.push(model); + } - trace!("[Model Selection]: Available models: {:?}", models); - - let mut current_active_local_ai_model = None; - - // If user enable local ai, then add local ai model to the list. - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - let model = AIModel::local(local_model, "".to_string()); - current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); - models.push(model); - } - - if models.is_empty() { - return Ok(AvailableModelsPB { + Ok(AvailableModelsPB { models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::default(), - }); - } + selected_model: AIModelPB::from(selected_model), + }) + } else { + // Build the models list from server models and mark them as non-local. + let mut models: Vec = self + .get_server_available_models() + .await? + .into_iter() + .map(AIModel::from) + .collect(); - // Global active model is the model selected by the user in the workspace settings. - let mut server_active_model = self - .get_workspace_select_model() - .await - .map(|m| AIModel::server(m, "".to_string())) - .unwrap_or_else(|_| AIModel::default()); + trace!("[Model Selection]: Available models: {:?}", models); + let mut current_active_local_ai_model = None; - trace!( - "[Model Selection] server active model: {:?}", - server_active_model - ); + // If user enable local ai, then add local ai model to the list. + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + let model = AIModel::local(local_model, "".to_string()); + current_active_local_ai_model = Some(model.clone()); + trace!("[Model Selection] current local ai model: {}", model.name); + models.push(model); + } - let mut user_selected_model = server_active_model.clone(); - // when current select model is deprecated, reset the model to default - if !models.iter().any(|m| m.name == server_active_model.name) { - server_active_model = AIModel::default(); - } + if models.is_empty() { + return Ok(AvailableModelsPB { + models: models.into_iter().map(|m| m.into()).collect(), + selected_model: AIModelPB::default(), + }); + } - let source_key = ai_available_models_key(&source); + // Global active model is the model selected by the user in the workspace settings. + let mut server_active_model = self + .get_workspace_select_model() + .await + .map(|m| AIModel::server(m, "".to_string())) + .unwrap_or_else(|_| AIModel::default()); - // We use source to identify user selected model. source can be document id or chat id. - match self.store_preferences.get_object::(&source_key) { - None => { - // when there is selected model and current local ai is active, then use local ai - if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { - user_selected_model = local_ai_model.clone(); - } - }, - Some(mut model) => { - trace!("[Model Selection] user previous select model: {:?}", model); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. - if model.is_local { - if let Some(local_ai_model) = ¤t_active_local_ai_model { - if local_ai_model.name != model.name { - model = local_ai_model.clone(); + trace!( + "[Model Selection] server active model: {:?}", + server_active_model + ); + + let mut user_selected_model = server_active_model.clone(); + // when current select model is deprecated, reset the model to default + if !models.iter().any(|m| m.name == server_active_model.name) { + server_active_model = AIModel::default(); + } + + let source_key = ai_available_models_key(&source); + // We use source to identify user selected model. source can be document id or chat id. + match self.store_preferences.get_object::(&source_key) { + None => { + // when there is selected model and current local ai is active, then use local ai + if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { + user_selected_model = local_ai_model.clone(); + } + }, + Some(mut model) => { + trace!("[Model Selection] user previous select model: {:?}", model); + // If source is provided, try to get the user-selected model from the store. User selected + // model will be used as the active model if it exists. + if model.is_local { + if let Some(local_ai_model) = ¤t_active_local_ai_model { + if local_ai_model.name != model.name { + model = local_ai_model.clone(); + } } } - } - user_selected_model = model; - }, - } - - // If user selected model is not available in the list, use the global active model. - let active_model = models - .iter() - .find(|m| m.name == user_selected_model.name) - .cloned() - .or(Some(server_active_model.clone())); - - // Update the stored preference if a different model is used. - if let Some(ref active_model) = active_model { - if active_model.name != user_selected_model.name { - self - .store_preferences - .set_object::(&source_key, &active_model.clone())?; + user_selected_model = model; + }, } - } - trace!("[Model Selection] final active model: {:?}", active_model); - let selected_model = AIModelPB::from(active_model.unwrap_or_default()); - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model, - }) + // If user selected model is not available in the list, use the global active model. + let active_model = models + .iter() + .find(|m| m.name == user_selected_model.name) + .cloned() + .or(Some(server_active_model.clone())); + + // Update the stored preference if a different model is used. + if let Some(ref active_model) = active_model { + if active_model.name != user_selected_model.name { + self + .store_preferences + .set_object::(&source_key, &active_model.clone())?; + } + } + + trace!("[Model Selection] final active model: {:?}", active_model); + let selected_model = AIModelPB::from(active_model.unwrap_or_default()); + Ok(AvailableModelsPB { + models: models.into_iter().map(|m| m.into()).collect(), + selected_model, + }) + } } pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result, FlowyError> { diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index a8280f9f66..c8c93a7f4c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -8,7 +8,7 @@ use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; -use flowy_error::FlowyError; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::ViewLayout; use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams}; use flowy_folder_pub::query::FolderService; @@ -164,11 +164,16 @@ impl ChatUserServiceImpl { } } +#[async_trait] impl AIUserService for ChatUserServiceImpl { fn user_id(&self) -> Result { self.upgrade_user()?.user_id() } + async fn is_local_model(&self) -> FlowyResult { + self.upgrade_user()?.is_local_mode().await + } + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 7584628056..1cf748d089 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -37,6 +37,7 @@ use crate::log_filter::init_log; use crate::server_layer::{current_server_type, Server, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; use user_state_callback::UserStatusCallbackImpl; pub mod config; @@ -333,6 +334,8 @@ impl ServerUserImpl { Ok(user) } } + +#[async_trait] impl ServerUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() @@ -342,6 +345,10 @@ impl ServerUser for ServerUserImpl { self.upgrade_user()?.user_id() } + async fn is_local_mode(&self) -> FlowyResult { + self.upgrade_user()?.is_local_mode().await + } + fn get_sqlite_db(&self, uid: i64) -> Result { self.upgrade_user()?.get_sqlite_connection(uid) } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index e194f614cd..3f5d16b165 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -13,6 +13,7 @@ use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; +use lib_infra::async_trait::async_trait; use serde_repr::*; use std::fmt::{Display, Formatter}; use std::path::PathBuf; @@ -184,11 +185,16 @@ pub fn current_server_type() -> Server { struct AIUserServiceImpl(Arc); +#[async_trait] impl AIUserService for AIUserServiceImpl { fn user_id(&self) -> Result { self.0.user_id() } + async fn is_local_model(&self) -> FlowyResult { + self.0.is_local_mode().await + } + fn workspace_id(&self) -> Result { self.0.workspace_id() } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index cf98cf5214..e6d8a7fac2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,5 +1,6 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; use std::path::PathBuf; use uuid::Uuid; @@ -9,11 +10,13 @@ pub const USER_EMAIL: &str = "email"; pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. +#[async_trait] pub trait ServerUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; fn user_id(&self) -> FlowyResult; + async fn is_local_mode(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 3004ce2163..11027028ff 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -12,6 +12,7 @@ use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; /// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: /// @@ -38,6 +39,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc )) } +#[async_trait] struct FakeServerUserImpl; impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { @@ -48,6 +50,10 @@ impl ServerUser for FakeServerUserImpl { todo!() } + async fn is_local_mode(&self) -> FlowyResult { + Ok(true) + } + fn get_sqlite_db(&self, _uid: i64) -> Result { todo!() } diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 7ec63f93f7..84c1e9afe9 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -3,6 +3,7 @@ use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; use collab_integrate::CollabKVDB; +use crate::user_manager::manager_history_user::ANON_USER; use arc_swap::ArcSwapOption; use collab_plugins::local_storage::kv::doc::CollabKVAction; use collab_plugins::local_storage::kv::KVTransactionDB; @@ -46,6 +47,17 @@ impl AuthenticateUser { Ok(session.user_id) } + pub async fn is_local_mode(&self) -> FlowyResult { + let uid = self.user_id()?; + if let Ok(anon_user) = self.get_anon_user().await { + if anon_user == uid { + return Ok(true); + } + } + + Ok(false) + } + pub fn device_id(&self) -> FlowyResult { Ok(self.user_config.device_id.to_string()) } @@ -150,4 +162,16 @@ impl AuthenticateUser { }, } } + + async fn get_anon_user(&self) -> FlowyResult { + let anon_session = self + .store_preferences + .get_object::(ANON_USER) + .ok_or(FlowyError::new( + ErrorCode::RecordNotFound, + "Anon user not found", + ))?; + + Ok(anon_session.user_id) + } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index 8d20bae427..b7f4789f9a 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -9,7 +9,7 @@ use flowy_user_pub::entities::Authenticator; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; -const ANON_USER: &str = "anon_user"; +pub const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] pub async fn get_migration_user( From ed64719560e71c399db522b4165f02836f3eaafe Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 21:09:24 +0800 Subject: [PATCH 347/384] chore: clippy --- .../lib/user/presentation/screens/workspace_error_screen.dart | 1 - frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index bd32696514..af6d4ad770 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 11027028ff..f196e44ea9 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -39,8 +39,9 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc )) } -#[async_trait] struct FakeServerUserImpl; + +#[async_trait] impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() From 5436277ada39450fd9cac00068bc2338ed455139 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 21:39:49 +0800 Subject: [PATCH 348/384] chore: fmt --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index eda95c29f7..9504d126ed 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -126,15 +126,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 31d8653ba61f5885630f7bad13e2d08f4e0968d1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 13:20:05 +0800 Subject: [PATCH 349/384] refactor: save chat and chat message --- .../tests/chat/chat_message_test.rs | 2 - .../tests/sql_test/chat_message_test.rs | 43 ++- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 5 +- .../src/persistence/chat_message_sql.rs | 36 ++- .../flowy-ai-pub/src/persistence/chat_sql.rs | 53 ++-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 38 +-- frontend/rust-lib/flowy-ai/src/chat.rs | 37 +-- frontend/rust-lib/flowy-ai/src/lib.rs | 1 + .../src/middleware/chat_service_mw.rs | 23 +- frontend/rust-lib/flowy-ai/src/offline/mod.rs | 1 + .../src/offline/offline_message_sync.rs | 258 ++++++++++++++++++ .../src/deps_resolve/cloud_service_impl.rs | 17 +- frontend/rust-lib/flowy-core/src/lib.rs | 4 +- .../rust-lib/flowy-core/src/server_layer.rs | 36 +-- .../flowy-server/src/af_cloud/define.rs | 29 +- .../flowy-server/src/af_cloud/impls/chat.rs | 17 +- .../src/af_cloud/impls/database.rs | 8 +- .../src/af_cloud/impls/document.rs | 8 +- .../flowy-server/src/af_cloud/impls/folder.rs | 8 +- .../af_cloud/impls/user/cloud_service_impl.rs | 6 +- .../flowy-server/src/af_cloud/impls/util.rs | 4 +- .../flowy-server/src/af_cloud/server.rs | 34 ++- .../src/local_server/impls/chat.rs | 102 ++++--- .../flowy-server/src/local_server/server.rs | 13 +- .../flowy-server/tests/af_cloud_test/util.rs | 4 +- .../down.sql | 3 + .../up.sql | 5 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 +- 28 files changed, 548 insertions(+), 271 deletions(-) create mode 100644 frontend/rust-lib/flowy-ai/src/offline/mod.rs create mode 100644 frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index c5b30d68c3..5b29258142 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -27,7 +27,6 @@ async fn af_cloud_create_chat_message_test() { &Uuid::from_str(&chat_id).unwrap(), &format!("hello world {}", i), ChatMessageType::System, - &[], ) .await .unwrap(); @@ -83,7 +82,6 @@ async fn af_cloud_load_remote_system_message_test() { &Uuid::from_str(&chat_id).unwrap(), &format!("hello server {}", i), ChatMessageType::System, - &[], ) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs index 549548ccf1..3294ad26db 100644 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs @@ -2,8 +2,8 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_ai_pub::cloud::MessageCursor; use flowy_ai_pub::persistence::{ - insert_chat_messages, select_chat_messages, select_message, select_message_content, - select_message_where_match_reply_message_id, total_message_count, ChatMessageTable, + select_answer_where_match_reply_message_id, select_chat_messages, select_message, + select_message_content, total_message_count, upsert_chat_messages, ChatMessageTable, }; use uuid::Uuid; @@ -31,6 +31,7 @@ async fn chat_message_table_insert_select_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ChatMessageTable { message_id: message_id_2, @@ -41,11 +42,12 @@ async fn chat_message_table_insert_select_test() { author_id: "ai".to_string(), reply_message_id: Some(message_id_1), metadata: Some(r#"{"source": "test"}"#.to_string()), + is_sync: false, }, ]; // Test insert_chat_messages - let result = insert_chat_messages(db_conn, &messages); + let result = upsert_chat_messages(db_conn, &messages); assert!( result.is_ok(), "Failed to insert chat messages: {:?}", @@ -105,11 +107,12 @@ async fn chat_message_table_cursor_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }); } // Insert messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Test MessageCursor::Offset let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -173,6 +176,7 @@ async fn chat_message_total_count_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ChatMessageTable { message_id: 1002, @@ -183,11 +187,12 @@ async fn chat_message_total_count_test() { author_id: "ai".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ]; // Insert messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Test total_message_count let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -205,9 +210,10 @@ async fn chat_message_total_count_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; - insert_chat_messages(db_conn, &[additional_message]).unwrap(); + upsert_chat_messages(db_conn, &[additional_message]).unwrap(); // Verify count increased let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -242,10 +248,11 @@ async fn chat_message_select_message_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Test select_message let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -294,10 +301,11 @@ async fn chat_message_select_content_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Test select_message_content let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -334,6 +342,7 @@ async fn chat_message_reply_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; let answer = ChatMessageTable { @@ -345,14 +354,15 @@ async fn chat_message_reply_test() { author_id: "ai".to_string(), reply_message_id: Some(question_id), // Link to question metadata: None, + is_sync: false, }; // Insert messages - insert_chat_messages(db_conn, &[question, answer]).unwrap(); + upsert_chat_messages(db_conn, &[question, answer]).unwrap(); // Test select_message_where_match_reply_message_id let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); + let result = select_answer_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); assert!(result.is_some()); let reply = result.unwrap(); @@ -362,7 +372,7 @@ async fn chat_message_reply_test() { // Test with non-existent reply relation let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_reply = select_message_where_match_reply_message_id( + let no_reply = select_answer_where_match_reply_message_id( db_conn, &chat_id, 9999, // Non-existent question ID ) .unwrap(); @@ -372,7 +382,7 @@ async fn chat_message_reply_test() { // Test with wrong chat_id let db_conn = test.user_manager.db_connection(uid).unwrap(); let wrong_chat = - select_message_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); + select_answer_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); assert!(wrong_chat.is_none()); } @@ -399,10 +409,11 @@ async fn chat_message_upsert_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Check original content let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -420,10 +431,11 @@ async fn chat_message_upsert_test() { author_id: "user_1".to_string(), reply_message_id: Some(1000), // Added reply ID metadata: Some(r#"{"updated": true}"#.to_string()), + is_sync: false, }; // Upsert message - insert_chat_messages(db_conn, &[updated_message]).unwrap(); + upsert_chat_messages(db_conn, &[updated_message]).unwrap(); // Verify update let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -474,11 +486,12 @@ async fn chat_message_select_with_large_dataset() { } else { None }, + is_sync: false, }); } // Insert all 100 messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Verify total count let db_conn = test.user_manager.db_connection(uid).unwrap(); diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 0acf3c5bd3..b91021142e 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -95,7 +95,6 @@ pub trait ChatCloudService: Send + Sync + 'static { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result; async fn create_answer( @@ -111,7 +110,7 @@ pub trait ChatCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result; @@ -120,7 +119,7 @@ pub trait ChatCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result; async fn get_chat_messages( diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index f5f985575a..230e5761d2 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -1,4 +1,5 @@ use crate::cloud::MessageCursor; +use client_api::entity::chat_dto::ChatMessage; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::upsert::excluded; use flowy_sqlite::{ @@ -21,9 +22,40 @@ pub struct ChatMessageTable { pub author_id: String, pub reply_message_id: Option, pub metadata: Option, + pub is_sync: bool, +} +impl ChatMessageTable { + pub fn from_message(chat_id: String, message: ChatMessage, is_sync: bool) -> Self { + ChatMessageTable { + message_id: message.message_id, + chat_id, + content: message.content, + created_at: message.created_at.timestamp(), + author_type: message.author.author_type as i64, + author_id: message.author.author_id.to_string(), + reply_message_id: message.reply_message_id, + metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), + is_sync, + } + } } -pub fn insert_chat_messages( +pub fn update_chat_message_is_sync( + mut conn: DBConnection, + chat_id_val: &str, + message_id_val: i64, + is_sync_val: bool, +) -> FlowyResult<()> { + diesel::update(chat_message_table::table) + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .filter(chat_message_table::message_id.eq(message_id_val)) + .set(chat_message_table::is_sync.eq(is_sync_val)) + .execute(&mut *conn)?; + + Ok(()) +} + +pub fn upsert_chat_messages( mut conn: DBConnection, new_messages: &[ChatMessageTable], ) -> FlowyResult<()> { @@ -143,7 +175,7 @@ pub fn select_message_content( Ok(message) } -pub fn select_message_where_match_reply_message_id( +pub fn select_answer_where_match_reply_message_id( mut conn: DBConnection, chat_id: &str, answer_message_id_val: i64, diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs index 218f6222c3..f5398c48c0 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs @@ -7,7 +7,10 @@ use flowy_sqlite::{ schema::{chat_table, chat_table::dsl}, AsChangeset, DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, }; +use lib_infra::util::timestamp; use serde::{Deserialize, Serialize}; +use serde_json::Value; +use uuid::Uuid; #[derive(Clone, Default, Queryable, Insertable, Identifiable)] #[diesel(table_name = chat_table)] @@ -18,6 +21,23 @@ pub struct ChatTable { pub name: String, pub metadata: String, pub rag_ids: Option, + pub is_sync: bool, +} + +impl ChatTable { + pub fn new(chat_id: String, metadata: Value, rag_ids: Vec, is_sync: bool) -> Self { + let rag_ids = rag_ids.iter().map(|v| v.to_string()).collect::>(); + let metadata = serialize_chat_metadata(&metadata); + let rag_ids = Some(serialize_rag_ids(&rag_ids)); + Self { + chat_id, + created_at: timestamp(), + name: "".to_string(), + metadata, + rag_ids, + is_sync, + } + } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -49,27 +69,7 @@ pub struct ChatTableChangeset { pub name: Option, pub metadata: Option, pub rag_ids: Option, -} - -impl ChatTableChangeset { - pub fn from_metadata(metadata: ChatTableMetadata) -> Self { - ChatTableChangeset { - chat_id: Default::default(), - metadata: serde_json::to_string(&metadata).ok(), - name: None, - rag_ids: None, - } - } - - pub fn from_rag_ids(rag_ids: Vec) -> Self { - ChatTableChangeset { - chat_id: Default::default(), - // Serialize the Vec to a JSON array string - rag_ids: Some(serde_json::to_string(&rag_ids).unwrap_or_default()), - name: None, - metadata: None, - } - } + pub is_sync: Option, } pub fn serialize_rag_ids(rag_ids: &[String]) -> String { @@ -107,6 +107,7 @@ pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< chat_table::name.eq(excluded(chat_table::name)), chat_table::metadata.eq(excluded(chat_table::metadata)), chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), + chat_table::is_sync.eq(excluded(chat_table::is_sync)), )) .execute(&mut *conn) } @@ -120,6 +121,16 @@ pub fn update_chat( Ok(affected_row) } +pub fn update_chat_is_sync( + mut conn: DBConnection, + chat_id_val: &str, + is_sync_val: bool, +) -> QueryResult { + diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) + .set(chat_table::is_sync.eq(is_sync_val)) + .execute(&mut *conn) +} + pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { let row = dsl::chat_table .filter(chat_table::chat_id.eq(chat_id_val)) diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index c2f5b223c0..7602ff8040 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -4,10 +4,8 @@ use crate::entities::{ FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; use crate::local_ai::controller::{LocalAIController, LocalAISetting}; -use crate::middleware::chat_service_mw::AICloudServiceMiddleware; -use flowy_ai_pub::persistence::{ - read_chat_metadata, serialize_chat_metadata, serialize_rag_ids, upsert_chat, ChatTable, -}; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; +use flowy_ai_pub::persistence::read_chat_metadata; use std::collections::HashMap; use dashmap::DashMap; @@ -72,7 +70,7 @@ struct ServerModelsCache { pub const GLOBAL_ACTIVE_MODEL_KEY: &str = "global_active_model"; pub struct AIManager { - pub cloud_service_wm: Arc, + pub cloud_service_wm: Arc, pub user_service: Arc, pub external_service: Arc, chats: Arc>>, @@ -97,7 +95,7 @@ impl AIManager { }); let external_service = Arc::new(query_service); - let cloud_service_wm = Arc::new(AICloudServiceMiddleware::new( + let cloud_service_wm = Arc::new(ChatServiceMiddleware::new( user_service.clone(), chat_cloud_service, local_ai.clone(), @@ -226,13 +224,6 @@ impl AIManager { .unwrap_or_default(); info!("[Chat] create chat with rag_ids: {:?}", rag_ids); - save_chat( - self.user_service.sqlite_connection(*uid)?, - chat_id, - "", - rag_ids.iter().map(|v| v.to_string()).collect(), - json!({}), - )?; self .cloud_service_wm .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) @@ -730,28 +721,9 @@ async fn sync_chat_documents( Ok(()) } -fn save_chat( - conn: DBConnection, - chat_id: &Uuid, - name: &str, - rag_ids: Vec, - metadata: serde_json::Value, -) -> FlowyResult<()> { - let row = ChatTable { - chat_id: chat_id.to_string(), - created_at: timestamp(), - name: name.to_string(), - metadata: serialize_chat_metadata(&metadata), - rag_ids: Some(serialize_rag_ids(&rag_ids)), - }; - - upsert_chat(conn, &row)?; - Ok(()) -} - async fn refresh_chat_setting( user_service: &Arc, - cloud_service: &Arc, + cloud_service: &Arc, store_preferences: &Arc, chat_id: &Uuid, ) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 976acee3e4..ba5ff431e9 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -3,7 +3,7 @@ use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::middleware::chat_service_mw::AICloudServiceMiddleware; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; @@ -11,7 +11,7 @@ use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; use flowy_ai_pub::persistence::{ - insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages, ChatMessageTable, }; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -35,7 +35,7 @@ pub struct Chat { chat_id: Uuid, uid: i64, user_service: Arc, - chat_service: Arc, + chat_service: Arc, prev_message_state: Arc>, latest_message_id: Arc, stop_stream: Arc, @@ -47,7 +47,7 @@ impl Chat { uid: i64, chat_id: Uuid, user_service: Arc, - chat_service: Arc, + chat_service: Arc, ) -> Chat { Chat { uid, @@ -105,7 +105,6 @@ impl Chat { &self.chat_id, ¶ms.message, params.message_type.clone(), - &[], ) .await .map_err(|err| { @@ -126,7 +125,7 @@ impl Chat { } // Save message to disk - save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; + notify_message(&self.chat_id, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); self.stream_response( params.answer_stream_port, @@ -185,7 +184,7 @@ impl Chat { &self, answer_stream_port: i64, answer_stream_buffer: Arc>, - uid: i64, + _uid: i64, workspace_id: Uuid, question_id: i64, format: ResponseFormat, @@ -194,7 +193,6 @@ impl Chat { let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id; let cloud_service = self.chat_service.clone(); - let user_service = self.user_service.clone(); tokio::spawn(async move { let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service @@ -309,7 +307,7 @@ impl Chat { metadata, ) .await?; - save_and_notify_message(uid, &chat_id, &user_service, answer)?; + notify_message(&chat_id, answer)?; Ok::<(), FlowyError>(()) }); } @@ -442,6 +440,7 @@ impl Chat { user_service.sqlite_connection(uid)?, &chat_id, resp.messages.clone(), + true, ) { error!("Failed to save chat:{} messages: {}", chat_id, err); } @@ -492,7 +491,7 @@ impl Chat { let conn = self.user_service.sqlite_connection(self.uid)?; let local_result = - select_message_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? + select_answer_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? .map(|message| message.message_id); if let Some(message_id) = local_result { @@ -543,7 +542,7 @@ impl Chat { .get_answer(&workspace_id, &self.chat_id, question_message_id) .await?; - save_and_notify_message(self.uid, &self.chat_id, &self.user_service, answer.clone())?; + notify_message(&self.chat_id, answer.clone())?; let pb = ChatMessagePB::from(answer); Ok(pb) } @@ -614,6 +613,7 @@ fn save_chat_message_disk( conn: DBConnection, chat_id: &Uuid, messages: Vec, + is_sync: bool, ) -> FlowyResult<()> { let records = messages .into_iter() @@ -626,9 +626,10 @@ fn save_chat_message_disk( author_id: message.author.author_id.to_string(), reply_message_id: message.reply_message_id, metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), + is_sync, }) .collect::>(); - insert_chat_messages(conn, &records)?; + upsert_chat_messages(conn, &records)?; Ok(()) } @@ -665,18 +666,8 @@ impl StringBuffer { } } -pub(crate) fn save_and_notify_message( - uid: i64, - chat_id: &Uuid, - user_service: &Arc, - message: ChatMessage, -) -> Result<(), FlowyError> { +pub(crate) fn notify_message(chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { trace!("[Chat] save answer: answer={:?}", message); - save_chat_message_disk( - user_service.sqlite_connection(uid)?, - chat_id, - vec![message.clone()], - )?; let pb = ChatMessagePB::from(message); chat_notification_builder(chat_id, ChatNotification::DidReceiveChatMessage) .payload(pb) diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 400afd1507..5b582b2577 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -12,6 +12,7 @@ pub mod local_ai; mod middleware; pub mod notification; +pub mod offline; mod protobuf; mod stream_message; mod util; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index e3c3d671ec..ff5c608f22 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -28,14 +28,14 @@ use std::sync::{Arc, Weak}; use tracing::{info, trace}; use uuid::Uuid; -pub struct AICloudServiceMiddleware { +pub struct ChatServiceMiddleware { cloud_service: Arc, user_service: Arc, local_ai: Arc, storage_service: Weak, } -impl AICloudServiceMiddleware { +impl ChatServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, @@ -106,7 +106,7 @@ impl AICloudServiceMiddleware { } #[async_trait] -impl ChatCloudService for AICloudServiceMiddleware { +impl ChatCloudService for ChatServiceMiddleware { async fn create_chat( &self, uid: &i64, @@ -128,11 +128,10 @@ impl ChatCloudService for AICloudServiceMiddleware { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { self .cloud_service - .create_question(workspace_id, chat_id, message, message_type, metadata) + .create_question(workspace_id, chat_id, message, message_type) .await } @@ -154,7 +153,7 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result { @@ -166,7 +165,7 @@ impl ChatCloudService for AICloudServiceMiddleware { info!("stream_answer use model: {:?}", ai_model); if use_local_ai { if self.local_ai.is_running() { - let content = self.get_message_content(message_id)?; + let content = self.get_message_content(question_id)?; match self .local_ai .stream_question( @@ -191,7 +190,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .stream_answer(workspace_id, chat_id, message_id, format, ai_model) + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) .await } } @@ -200,10 +199,10 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { if self.local_ai.is_running() { - let content = self.get_message_content(question_message_id)?; + let content = self.get_message_content(question_id)?; match self .local_ai .ask_question(&chat_id.to_string(), &content) @@ -212,7 +211,7 @@ impl ChatCloudService for AICloudServiceMiddleware { Ok(answer) => { let message = self .cloud_service - .create_answer(workspace_id, chat_id, &answer, question_message_id, None) + .create_answer(workspace_id, chat_id, &answer, question_id, None) .await?; Ok(message) }, @@ -224,7 +223,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .get_answer(workspace_id, chat_id, question_message_id) + .get_answer(workspace_id, chat_id, question_id) .await } } diff --git a/frontend/rust-lib/flowy-ai/src/offline/mod.rs b/frontend/rust-lib/flowy-ai/src/offline/mod.rs new file mode 100644 index 0000000000..e55b43fdb2 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/offline/mod.rs @@ -0,0 +1 @@ +pub mod offline_message_sync; diff --git a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs new file mode 100644 index 0000000000..8d7e8d2e42 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs @@ -0,0 +1,258 @@ +use crate::ai_manager::AIUserService; +use flowy_ai_pub::cloud::{ + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, + MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, + StreamAnswer, StreamComplete, UpdateChatParams, +}; +use flowy_ai_pub::persistence::{ + update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages, + ChatMessageTable, ChatTable, +}; +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; +use serde_json::Value; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use uuid::Uuid; + +pub struct AutoSyncChatService { + cloud_service: Arc, + user_service: Arc, +} + +impl AutoSyncChatService { + pub fn new( + cloud_service: Arc, + user_service: Arc, + ) -> Self { + Self { + cloud_service, + user_service, + } + } + + async fn upsert_message( + &self, + chat_id: &Uuid, + message: ChatMessage, + is_sync: bool, + ) -> Result<(), FlowyError> { + let uid = self.user_service.user_id()?; + let conn = self.user_service.sqlite_connection(uid)?; + let row = ChatMessageTable::from_message(chat_id.to_string(), message, is_sync); + upsert_chat_messages(conn, &[row])?; + Ok(()) + } + + #[allow(dead_code)] + async fn update_message_is_sync( + &self, + chat_id: &Uuid, + message_id: i64, + ) -> Result<(), FlowyError> { + let uid = self.user_service.user_id()?; + let conn = self.user_service.sqlite_connection(uid)?; + update_chat_message_is_sync(conn, &chat_id.to_string(), message_id, true)?; + Ok(()) + } +} + +#[async_trait] +impl ChatCloudService for AutoSyncChatService { + async fn create_chat( + &self, + uid: &i64, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, + name: &str, + metadata: Value, + ) -> Result<(), FlowyError> { + let conn = self.user_service.sqlite_connection(*uid)?; + let chat = ChatTable::new( + chat_id.to_string(), + metadata.clone(), + rag_ids.clone(), + false, + ); + upsert_chat(conn, &chat)?; + + if self + .cloud_service + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) + .await + .is_ok() + { + let conn = self.user_service.sqlite_connection(*uid)?; + update_chat_is_sync(conn, &chat_id.to_string(), true)?; + } + Ok(()) + } + + async fn create_question( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + message_type: ChatMessageType, + ) -> Result { + let message = self + .cloud_service + .create_question(workspace_id, chat_id, message, message_type) + .await?; + self.upsert_message(chat_id, message.clone(), true).await?; + // TODO: implement background sync + // self + // .update_message_is_sync(chat_id, message.message_id) + // .await?; + Ok(message) + } + + async fn create_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + question_id: i64, + metadata: Option, + ) -> Result { + let message = self + .cloud_service + .create_answer(workspace_id, chat_id, message, question_id, metadata) + .await?; + + // TODO: implement background sync + self.upsert_message(chat_id, message.clone(), true).await?; + Ok(message) + } + + async fn stream_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + question_id: i64, + format: ResponseFormat, + ai_model: Option, + ) -> Result { + self + .cloud_service + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) + .await + } + + async fn get_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + question_id: i64, + ) -> Result { + let message = self + .cloud_service + .get_answer(workspace_id, chat_id, question_id) + .await?; + + // TODO: implement background sync + self.upsert_message(chat_id, message.clone(), true).await?; + Ok(message) + } + + async fn get_chat_messages( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + offset: MessageCursor, + limit: u64, + ) -> Result { + self + .cloud_service + .get_chat_messages(workspace_id, chat_id, offset, limit) + .await + } + + async fn get_question_from_answer_id( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + answer_message_id: i64, + ) -> Result { + self + .cloud_service + .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) + .await + } + + async fn get_related_message( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message_id: i64, + ai_model: Option, + ) -> Result { + self + .cloud_service + .get_related_message(workspace_id, chat_id, message_id, ai_model) + .await + } + + async fn stream_complete( + &self, + workspace_id: &Uuid, + params: CompleteTextParams, + ai_model: Option, + ) -> Result { + self + .cloud_service + .stream_complete(workspace_id, params, ai_model) + .await + } + + async fn embed_file( + &self, + workspace_id: &Uuid, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, + ) -> Result<(), FlowyError> { + self + .cloud_service + .embed_file(workspace_id, file_path, chat_id, metadata) + .await + } + + async fn get_chat_settings( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + ) -> Result { + // TODO: implement background sync + self + .cloud_service + .get_chat_settings(workspace_id, chat_id) + .await + } + + async fn update_chat_settings( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + params: UpdateChatParams, + ) -> Result<(), FlowyError> { + // TODO: implement background sync + self + .cloud_service + .update_chat_settings(workspace_id, chat_id, params) + .await + } + + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + self.cloud_service.get_available_models(workspace_id).await + } + + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { + self + .cloud_service + .get_workspace_default_model(workspace_id) + .await + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index e4bffbfb69..3a7d648133 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -14,9 +14,9 @@ use flowy_ai_pub::cloud::search_dto::{ SearchDocumentResponseItem, SearchResult, SearchSummaryResult, }; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, UpdateChatParams, + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, + MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, + UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -683,13 +683,12 @@ impl ChatCloudService for ServerProvider { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { let message = message.to_string(); self .get_server()? .chat_service() - .create_question(workspace_id, chat_id, &message, message_type, metadata) + .create_question(workspace_id, chat_id, &message, message_type) .await } @@ -712,14 +711,14 @@ impl ChatCloudService for ServerProvider { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result { let server = self.get_server()?; server .chat_service() - .stream_answer(workspace_id, chat_id, message_id, format, ai_model) + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) .await } @@ -768,12 +767,12 @@ impl ChatCloudService for ServerProvider { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { let server = self.get_server(); server? .chat_service() - .get_answer(workspace_id, chat_id, question_message_id) + .get_answer(workspace_id, chat_id, question_id) .await } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 1cf748d089..dbbce02136 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::LoginUserService; use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -336,7 +336,7 @@ impl ServerUserImpl { } #[async_trait] -impl ServerUser for ServerUserImpl { +impl LoginUserService for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 3f5d16b165..a3d6de7003 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -2,24 +2,19 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; use arc_swap::ArcSwapOption; use dashmap::DashMap; -use flowy_ai::ai_manager::AIUserService; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::{AIUserServiceImpl, LoginUserService}; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; -use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; -use lib_infra::async_trait::async_trait; use serde_repr::*; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -use uuid::Uuid; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -61,7 +56,7 @@ pub struct ServerProvider { /// The authenticator type of the user. authenticator: AtomicU8, - user: Arc, + user: Arc, pub(crate) uid: Arc>, pub local_ai: Arc, } @@ -71,7 +66,7 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, - server_user: impl ServerUser + 'static, + server_user: impl LoginUserService + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); @@ -182,28 +177,3 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } - -struct AIUserServiceImpl(Arc); - -#[async_trait] -impl AIUserService for AIUserServiceImpl { - fn user_id(&self) -> Result { - self.0.user_id() - } - - async fn is_local_model(&self) -> FlowyResult { - self.0.is_local_mode().await - } - - fn workspace_id(&self) -> Result { - self.0.workspace_id() - } - - fn sqlite_connection(&self, uid: i64) -> Result { - self.0.get_sqlite_db(uid) - } - - fn application_root_dir(&self) -> Result { - self.0.application_root_dir() - } -} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index e6d8a7fac2..0cebe3371f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,7 +1,9 @@ +use flowy_ai::ai_manager::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; use std::path::PathBuf; +use std::sync::Arc; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -11,7 +13,7 @@ pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. #[async_trait] -pub trait ServerUser: Send + Sync { +pub trait LoginUserService: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; @@ -21,3 +23,28 @@ pub trait ServerUser: Send + Sync { fn get_sqlite_db(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; } + +pub struct AIUserServiceImpl(pub Arc); + +#[async_trait] +impl AIUserService for AIUserServiceImpl { + fn user_id(&self) -> Result { + self.0.user_id() + } + + async fn is_local_model(&self) -> FlowyResult { + self.0.is_local_mode().await + } + + fn workspace_id(&self) -> Result { + self.0.workspace_id() + } + + fn sqlite_connection(&self, uid: i64) -> Result { + self.0.get_sqlite_db(uid) + } + + fn application_root_dir(&self) -> Result { + self.0.application_root_dir() + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 3f8803c7fc..14a26078f5 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -8,8 +8,8 @@ use client_api::entity::chat_dto::{ RepeatedChatMessage, }; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - ModelList, StreamAnswer, StreamComplete, UpdateChatParams, + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, ModelList, StreamAnswer, + StreamComplete, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; @@ -20,12 +20,12 @@ use std::path::Path; use tracing::trace; use uuid::Uuid; -pub(crate) struct AFCloudChatCloudServiceImpl { +pub(crate) struct CloudChatServiceImpl { pub inner: T, } #[async_trait] -impl ChatCloudService for AFCloudChatCloudServiceImpl +impl ChatCloudService for CloudChatServiceImpl where T: AFServer, { @@ -59,7 +59,6 @@ where chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); @@ -132,15 +131,11 @@ where &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_answer( - workspace_id, - chat_id.to_string().as_str(), - question_message_id, - ) + .get_answer(workspace_id, chat_id.to_string().as_str(), question_id) .await .map_err(FlowyError::from)?; Ok(resp) diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index c493dda344..d6a22a2f73 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,5 +1,5 @@ #![allow(unused_variables)] -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ @@ -23,7 +23,7 @@ use uuid::Uuid; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -40,7 +40,7 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -95,7 +95,7 @@ where workspace_id: &Uuid, ) -> Result { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let client = try_get_client?; let params = object_ids .into_iter() diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 4909d96fef..ae67fc6d26 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -13,13 +13,13 @@ use std::sync::Arc; use tracing::instrument; use uuid::Uuid; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -49,7 +49,7 @@ where check_request_workspace_id_is_match( workspace_id, - &self.user, + &self.logged_user, format!("get document doc state:{}", document_id), )?; @@ -85,7 +85,7 @@ where .to_vec(); check_request_workspace_id_is_match( workspace_id, - &self.user, + &self.logged_user, format!("Get {} document", document_id), )?; let collab = Collab::new_with_source( diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 5b8efa4b32..116651a734 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -22,13 +22,13 @@ use flowy_folder_pub::cloud::{ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -91,7 +91,7 @@ where ) -> Result, FlowyError> { let uid = *uid; let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*workspace_id, CollabType::Folder), @@ -131,7 +131,7 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 6d7d9d743b..0ac4555d6e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -31,7 +31,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use uuid::Uuid; -use crate::af_cloud::define::{ServerUser, USER_SIGN_IN_URL}; +use crate::af_cloud::define::{LoginUserService, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; @@ -44,14 +44,14 @@ use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_st pub(crate) struct AFCloudUserAuthServiceImpl { server: T, user_change_recv: ArcSwapOption>, - user: Arc, + user: Arc, } impl AFCloudUserAuthServiceImpl { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver, - user: Arc, + user: Arc, ) -> Self { Self { server, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index 0d91de8412..bedcc90ca0 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use flowy_error::{FlowyError, FlowyResult}; use std::sync::Arc; use tracing::warn; @@ -9,7 +9,7 @@ use uuid::Uuid; /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( expected_workspace_id: &Uuid, - user: &Arc, + user: &Arc, action: impl AsRef, ) -> FlowyResult<()> { let actual_workspace_id = user.workspace_id()?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 06e56a8c05..bb2d11cdbd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::{AIUserServiceImpl, LoginUserService}; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -24,6 +24,11 @@ use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; use flowy_user_pub::entities::UserTokenState; +use crate::af_cloud::impls::{ + AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, + AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl, +}; +use flowy_ai::offline::offline_message_sync::AutoSyncChatService; use rand::Rng; use semver::Version; use tokio::select; @@ -34,11 +39,6 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use uuid::Uuid; -use crate::af_cloud::impls::{ - AFCloudChatCloudServiceImpl, AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, - AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, -}; - use crate::AppFlowyServer; use super::impls::AFCloudSearchCloudServiceImpl; @@ -53,7 +53,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc, pub device_id: String, ws_client: Arc, - user: Arc, + logged_user: Arc, } impl AppFlowyCloudServer { @@ -62,7 +62,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - user: Arc, + auth_user_service: Arc, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -100,7 +100,7 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - user, + logged_user: auth_user_service, } } @@ -187,7 +187,7 @@ impl AppFlowyServer for AppFlowyCloudServer { Arc::new(AFCloudUserAuthServiceImpl::new( server, rx, - self.user.clone(), + self.logged_user.clone(), )) } @@ -197,7 +197,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudFolderCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -207,7 +207,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudDatabaseCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -217,7 +217,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Some(Arc::new(AFCloudDatabaseCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), })) } @@ -227,7 +227,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudDocumentCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -235,7 +235,11 @@ impl AppFlowyServer for AppFlowyCloudServer { let server = AFServerImpl { client: self.get_client(), }; - Arc::new(AFCloudChatCloudServiceImpl { inner: server }) + + Arc::new(AutoSyncChatService::new( + Arc::new(CloudChatServiceImpl { inner: server }), + Arc::new(AIUserServiceImpl(self.logged_user.clone())), + )) } fn subscribe_ws_state(&self) -> Option { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 9057288f88..8e731edb84 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::CompletionStream; @@ -6,14 +6,15 @@ use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai::local_ai::stream_util::QuestionStream; use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, - RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, + ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, + ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_chat_messages, - select_message_content, select_message_where_match_reply_message_id, serialize_chat_metadata, - serialize_rag_ids, update_chat, upsert_chat, ChatMessageTable, ChatTable, ChatTableChangeset, + deserialize_chat_metadata, deserialize_rag_ids, read_chat, + select_answer_where_match_reply_message_id, select_chat_messages, select_message_content, + serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, upsert_chat_messages, + ChatMessageTable, ChatTable, ChatTableChangeset, }; use flowy_error::{FlowyError, FlowyResult}; use futures_util::{stream, StreamExt, TryStreamExt}; @@ -26,12 +27,12 @@ use std::sync::Arc; use tracing::trace; use uuid::Uuid; -pub struct LocalServerChatServiceImpl { - pub user: Arc, +pub struct LocalChatServiceImpl { + pub user: Arc, pub local_ai: Arc, } -impl LocalServerChatServiceImpl { +impl LocalChatServiceImpl { fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; @@ -40,35 +41,30 @@ impl LocalServerChatServiceImpl { })?; Ok(content) } + + async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { + let uid = self.user.user_id()?; + let conn = self.user.get_sqlite_db(uid)?; + let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); + upsert_chat_messages(conn, &[row])?; + Ok(()) + } } #[async_trait] -impl ChatCloudService for LocalServerChatServiceImpl { +impl ChatCloudService for LocalChatServiceImpl { async fn create_chat( &self, _uid: &i64, _workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, - name: &str, + _name: &str, metadata: Value, ) -> Result<(), FlowyError> { let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; - - let rag_ids = rag_ids - .iter() - .map(|v| v.to_string()) - .collect::>(); - - let row = ChatTable { - chat_id: chat_id.to_string(), - created_at: timestamp(), - name: name.to_string(), - metadata: serialize_chat_metadata(&metadata), - rag_ids: Some(serialize_rag_ids(&rag_ids)), - }; - + let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); upsert_chat(db, &row)?; Ok(()) } @@ -76,25 +72,23 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn create_question( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, - _metadata: &[ChatMessageMetadata], ) -> Result { - match message_type { - ChatMessageType::System => Ok(ChatMessage::new_system(timestamp(), message.to_string())), - ChatMessageType::User => Ok(ChatMessage::new_human( - timestamp(), - message.to_string(), - None, - )), - } + let message = match message_type { + ChatMessageType::System => ChatMessage::new_system(timestamp(), message.to_string()), + ChatMessageType::User => ChatMessage::new_human(timestamp(), message.to_string(), None), + }; + + self.upsert_message(chat_id, message.clone()).await?; + Ok(message) } async fn create_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -103,6 +97,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { if let Some(metadata) = metadata { message.metadata = metadata; } + self.upsert_message(chat_id, message.clone()).await?; Ok(message) } @@ -131,22 +126,26 @@ impl ChatCloudService for LocalServerChatServiceImpl { stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), ), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } async fn get_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _question_message_id: i64, + chat_id: &Uuid, + question_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + + match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { + None => Err(FlowyError::record_not_found()), + Some(message) => Ok(chat_message_from_row(message)), + } } async fn get_chat_messages( @@ -183,7 +182,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { let chat_id = chat_id.to_string(); let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; - let row = select_message_where_match_reply_message_id(db, &chat_id, answer_message_id)? + let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? .map(chat_message_from_row) .ok_or_else(FlowyError::record_not_found)?; Ok(row) @@ -247,12 +246,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } @@ -285,7 +282,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { let db = self.user.get_sqlite_db(uid)?; let row = read_chat(db, &chat_id)?; let rag_ids = deserialize_rag_ids(&row.rag_ids); - let metadata = deserialize_chat_metadata::(&row.metadata); + let metadata = deserialize_chat_metadata::(&row.metadata); let setting = ChatSettings { name: row.name, rag_ids, @@ -308,6 +305,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { name: s.name, metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), + is_sync: None, }; update_chat(&mut db, changeset)?; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 0ef930320e..719dd59c95 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,11 +1,10 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::local_server::impls::{ - LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, - LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, - LocalServerUserServiceImpl, + LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, + LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, }; use crate::AppFlowyServer; use flowy_ai::local_ai::controller::LocalAIController; @@ -18,13 +17,13 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, local_ai, @@ -62,7 +61,7 @@ impl AppFlowyServer for LocalServer { } fn chat_service(&self) -> Arc { - Arc::new(LocalServerChatServiceImpl { + Arc::new(LocalChatServiceImpl { user: self.user.clone(), local_ai: self.local_ai.clone(), }) diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index f196e44ea9..a8dcdb507f 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; use crate::setup_log; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::LoginUserService; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; @@ -42,7 +42,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc struct FakeServerUserImpl; #[async_trait] -impl ServerUser for FakeServerUserImpl { +impl LoginUserService for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql new file mode 100644 index 0000000000..65dec0f30a --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE chat_table DROP COLUMN is_sync; +ALTER TABLE chat_message_table DROP COLUMN is_sync; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql new file mode 100644 index 0000000000..ff8dce94bc --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE chat_table + ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE chat_message_table + ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 9504d126ed..f3caa86e66 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -27,6 +27,7 @@ diesel::table! { author_id -> Text, reply_message_id -> Nullable, metadata -> Nullable, + is_sync -> Bool, } } @@ -37,6 +38,7 @@ diesel::table! { name -> Text, metadata -> Text, rag_ids -> Nullable, + is_sync -> Bool, } } @@ -126,15 +128,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 165e95c480663f989b32a1be7d59dbc878f58525 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 14:36:47 +0800 Subject: [PATCH 350/384] chore: fix test --- .../event-integration-test/tests/chat/chat_message_test.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index 5b29258142..a9c928db5b 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -91,10 +91,8 @@ async fn af_cloud_load_remote_system_message_test() { .notification_sender .subscribe::(&chat_id, ChatNotification::DidLoadLatestChatMessage); - // Previous messages were created by the server, so there are no messages in the local cache. - // It will try to load messages in the background. let all = test.load_next_message(&chat_id, 5, None).await; - assert!(all.messages.is_empty()); + assert_eq!(all.messages.len(), 5); // Wait for the messages to be loaded. let next_back_five = receive_with_timeout(rx, Duration::from_secs(60)) @@ -119,7 +117,6 @@ async fn af_cloud_load_remote_system_message_test() { let first_five_messages = receive_with_timeout(rx, Duration::from_secs(60)) .await .unwrap(); - assert!(!first_five_messages.has_more); assert_eq!(first_five_messages.messages[0].content, "hello server 4"); assert_eq!(first_five_messages.messages[1].content, "hello server 3"); assert_eq!(first_five_messages.messages[2].content, "hello server 2"); From 59efb7d9e54d2c044721e2be6b884a86914cbce7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 14:43:01 +0800 Subject: [PATCH 351/384] chore: fix test --- frontend/rust-lib/Cargo.lock | 24 ++++++++++++------------ frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ec114f205a..fd0307fca7 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "async-trait", @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "bincode", "bytes", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3442,7 +3442,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "app-error", "jsonwebtoken", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bytes", @@ -6644,7 +6644,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index e4f0eab545..ee99fcacf1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -107,8 +107,8 @@ af-local-ai = { version = "0.1" } # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } [profile.dev] opt-level = 0 From 3bb5075a9830354965a2551feece6039f2f56c3f Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 18 Apr 2025 14:46:46 +0800 Subject: [PATCH 352/384] feat: setup/change password in settings page (#7752) * feat: change password in settings page * feat: add change password api * feat: add password service * feat: add setup password * feat: refacotor account page * chor: update i18n * chore: i18n * chore: i18n * feat: add error message under text field * fix: flutter tests * chore: remove long password test * fix: cloud integration test * fix: cargo clippy * fix: replace border color --- .../integration_test/shared/settings.dart | 2 +- .../personal_info_setting_group.dart | 2 +- .../application/password/password_bloc.dart | 241 +++++++++++++ .../password/password_http_service.dart | 183 ++++++++++ .../lib/user/application/sign_in_bloc.dart | 2 + .../lib/user/application/user_service.dart | 11 + .../continue_with_email_and_password.dart | 89 ++--- ...inue_with_magic_link_or_passcode_page.dart | 4 +- .../continue_with_password_page.dart | 43 ++- .../widgets/flowy_logo_title.dart | 2 +- .../application/user/settings_user_bloc.dart | 37 +- .../menu/sidebar/footer/sidebar_toast.dart | 6 +- .../menu/sidebar/shared/sidebar_setting.dart | 67 +++- .../settings/pages/about/app_version.dart | 30 +- .../pages/account/account_deletion.dart | 43 +-- .../pages/account/account_sign_in_out.dart | 105 +++++- .../pages/account/account_user_profile.dart | 21 +- .../pages/account/email/email_section.dart | 38 ++ .../account/password/change_password.dart | 330 ++++++++++++++++++ .../password/password_suffix_icon.dart | 30 ++ .../account/password/setup_password.dart | 254 ++++++++++++++ .../settings/pages/settings_account_view.dart | 17 +- .../settings/shared/settings_category.dart | 10 +- .../shared/settings_category_spacer.dart | 10 +- .../settings/shared/settings_header.dart | 12 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 +-- .../component/button/base_button/base.dart | 2 +- .../button/base_button/base_text_button.dart | 4 + .../filled_button/filled_text_button.dart | 10 +- .../outlined_button/outlined_text_button.dart | 10 +- .../src/component/textfield/textfield.dart | 31 ++ .../text_style/base/default_text_style.dart | 295 ++++++++++++++-- .../lib/src/theme/text_style/text_style.dart | 10 +- .../flowy_icons/20x/hide_password.svg | 5 + .../flowy_icons/20x/password_close.svg | 5 + .../flowy_icons/20x/show_password.svg | 4 + frontend/resources/translations/en.json | 36 +- frontend/rust-lib/Cargo.lock | 18 +- .../tests/user/local_test/auth_test.rs | 23 -- .../src/local_server/impls/chat.rs | 16 +- .../rust-lib/flowy-user/src/entities/auth.rs | 7 +- .../flowy-user/src/entities/user_profile.rs | 7 +- 42 files changed, 1854 insertions(+), 264 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart create mode 100644 frontend/resources/flowy_icons/20x/hide_password.svg create mode 100644 frontend/resources/flowy_icons/20x/password_close.svg create mode 100644 frontend/resources/flowy_icons/20x/show_password.svg diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index aade7bb4c9..bfc5efedde 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester { // Enable editing username final editUsernameFinder = find.descendant( of: find.byType(AccountUserProfile), - matching: find.byFlowySvg(FlowySvgs.edit_s), + matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m), ); await tap(editUsernameFinder, warnIfMissed: false); await pumpAndSettle(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index 37191a2ae2..28ebdb750e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -58,7 +58,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { userName: userName, onSubmitted: (value) => context .read() - .add(SettingsUserEvent.updateUserName(value)), + .add(SettingsUserEvent.updateUserName(name: value)), ); }, ); diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart new file mode 100644 index 0000000000..34e8514e4c --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -0,0 +1,241 @@ +import 'dart:convert'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/user/application/password/password_http_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/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'password_bloc.freezed.dart'; + +class PasswordBloc extends Bloc { + PasswordBloc(this.userProfile) : super(PasswordState.initial()) { + on( + (event, emit) async { + await event.when( + init: () async => _init(), + changePassword: (oldPassword, newPassword) async => _onChangePassword( + emit, + oldPassword: oldPassword, + newPassword: newPassword, + ), + setupPassword: (newPassword) async => _onSetupPassword( + emit, + newPassword: newPassword, + ), + forgotPassword: (email) async => _onForgotPassword( + emit, + email: email, + ), + checkHasPassword: () async => _onCheckHasPassword( + emit, + ), + cancel: () {}, + ); + }, + ); + } + + final UserProfilePB userProfile; + late final PasswordHttpService passwordHttpService; + + bool _isInitialized = false; + + Future _init() async { + if (userProfile.authenticator == AuthenticatorPB.Local) { + Log.debug('PasswordBloc: skip init because user is local authenticator'); + return; + } + + final baseUrl = await getAppFlowyCloudUrl(); + try { + final authToken = jsonDecode(userProfile.token)['access_token']; + passwordHttpService = PasswordHttpService( + baseUrl: baseUrl, + authToken: authToken, + ); + _isInitialized = true; + } catch (e) { + Log.error('PasswordBloc: _init: error: $e'); + } + } + + Future _onChangePassword( + Emitter emit, { + required String oldPassword, + required String newPassword, + }) async { + if (!_isInitialized) { + Log.info('changePassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('changePassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.changePassword( + currentPassword: oldPassword, + newPassword: newPassword, + ); + + emit( + state.copyWith( + isSubmitting: false, + changePasswordResult: result, + ), + ); + } + + Future _onSetupPassword( + Emitter emit, { + required String newPassword, + }) async { + if (!_isInitialized) { + Log.info('setupPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('setupPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.setupPassword( + newPassword: newPassword, + ); + + emit( + state.copyWith( + isSubmitting: false, + hasPassword: result.fold( + (success) => true, + (error) => false, + ), + setupPasswordResult: result, + ), + ); + } + + Future _onForgotPassword( + Emitter emit, { + required String email, + }) async { + if (!_isInitialized) { + Log.info('forgotPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('forgotPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.forgotPassword(email: email); + + emit( + state.copyWith( + isSubmitting: false, + forgotPasswordResult: result, + ), + ); + } + + Future _onCheckHasPassword(Emitter emit) async { + if (!_isInitialized) { + Log.info('checkHasPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('checkHasPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.checkHasPassword(); + + emit( + state.copyWith( + isSubmitting: false, + hasPassword: result.fold( + (success) => success, + (error) => false, + ), + checkHasPasswordResult: result, + ), + ); + } + + void _clearState(Emitter emit, bool isSubmitting) { + emit( + state.copyWith( + isSubmitting: isSubmitting, + changePasswordResult: null, + setupPasswordResult: null, + forgotPasswordResult: null, + checkHasPasswordResult: null, + ), + ); + } +} + +@freezed +class PasswordEvent with _$PasswordEvent { + const factory PasswordEvent.init() = Init; + + // Change password + const factory PasswordEvent.changePassword({ + required String oldPassword, + required String newPassword, + }) = ChangePassword; + + // Setup password + const factory PasswordEvent.setupPassword({ + required String newPassword, + }) = SetupPassword; + + // Forgot password + const factory PasswordEvent.forgotPassword({ + required String email, + }) = ForgotPassword; + + // Check has password + const factory PasswordEvent.checkHasPassword() = CheckHasPassword; + + // Cancel operation + const factory PasswordEvent.cancel() = Cancel; +} + +@freezed +class PasswordState with _$PasswordState { + const factory PasswordState({ + required bool isSubmitting, + required bool hasPassword, + required FlowyResult? changePasswordResult, + required FlowyResult? setupPasswordResult, + required FlowyResult? forgotPasswordResult, + required FlowyResult? checkHasPasswordResult, + }) = _PasswordState; + + factory PasswordState.initial() => const PasswordState( + isSubmitting: false, + hasPassword: false, + changePasswordResult: null, + setupPasswordResult: null, + forgotPasswordResult: null, + checkHasPasswordResult: null, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart new file mode 100644 index 0000000000..723ded57e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:http/http.dart' as http; + +enum PasswordEndpoint { + changePassword, + forgotPassword, + setupPassword, + checkHasPassword; + + String get path { + switch (this) { + case PasswordEndpoint.changePassword: + return '/gotrue/user/change-password'; + case PasswordEndpoint.forgotPassword: + return '/gotrue/user/recover'; + case PasswordEndpoint.setupPassword: + return '/gotrue/user/change-password'; + case PasswordEndpoint.checkHasPassword: + return '/gotrue/user/auth-info'; + } + } + + String get method { + switch (this) { + case PasswordEndpoint.changePassword: + case PasswordEndpoint.setupPassword: + case PasswordEndpoint.forgotPassword: + return 'POST'; + case PasswordEndpoint.checkHasPassword: + return 'GET'; + } + } + + Uri uri(String baseUrl) => Uri.parse('$baseUrl$path'); +} + +class PasswordHttpService { + PasswordHttpService({ + required this.baseUrl, + required this.authToken, + }); + + final String baseUrl; + final String authToken; + + final http.Client client = http.Client(); + + Map get headers => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $authToken', + }; + + /// Changes the user's password + /// + /// [currentPassword] - The user's current password + /// [newPassword] - The new password to set + Future> changePassword({ + required String currentPassword, + required String newPassword, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.changePassword, + body: { + 'current_password': currentPassword, + 'password': newPassword, + }, + errorMessage: 'Failed to change password', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Sends a password reset email to the user + /// + /// [email] - The email address of the user + Future> forgotPassword({ + required String email, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.forgotPassword, + body: {'email': email}, + errorMessage: 'Failed to send password reset email', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Sets up a password for a user that doesn't have one + /// + /// [newPassword] - The new password to set + Future> setupPassword({ + required String newPassword, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.setupPassword, + body: {'password': newPassword}, + errorMessage: 'Failed to setup password', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Checks if the user has a password set + Future> checkHasPassword() async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.checkHasPassword, + errorMessage: 'Failed to check password status', + ); + + return result.fold( + (data) => FlowyResult.success(data['has_password'] ?? false), + (error) => FlowyResult.failure(error), + ); + } + + /// Makes a request to the specified endpoint with the given body + Future> _makeRequest({ + required PasswordEndpoint endpoint, + Map? body, + String errorMessage = 'Request failed', + }) async { + try { + final uri = endpoint.uri(baseUrl); + http.Response response; + + if (endpoint.method == 'POST') { + response = await client.post( + uri, + headers: headers, + body: body != null ? jsonEncode(body) : null, + ); + } else if (endpoint.method == 'GET') { + response = await client.get( + uri, + headers: headers, + ); + } else { + return FlowyResult.failure( + FlowyError(msg: 'Invalid request method: ${endpoint.method}'), + ); + } + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + return FlowyResult.success(jsonDecode(response.body)); + } + return FlowyResult.success(true); + } else { + final errorBody = + response.body.isNotEmpty ? jsonDecode(response.body) : {}; + + Log.info( + '${endpoint.name} request failed: ${response.statusCode}, $errorBody ', + ); + + return FlowyResult.failure( + FlowyError( + msg: errorBody['msg'] ?? errorMessage, + ), + ); + } + } catch (e) { + Log.error('${endpoint.name} request failed: error: $e'); + + return FlowyResult.failure( + FlowyError(msg: 'Network error: ${e.toString()}'), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 339af51f9f..9691a1269b 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -303,6 +303,8 @@ class SignInBloc extends Bloc { msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); } else if (errorMsg.contains('invalid')) { msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); + } else if (errorMsg.contains('Invalid login credentials')) { + msg = LocaleKeys.signIn_invalidLoginCredentials.tr(); } return state.copyWith( isSubmitting: false, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 644a115641..4359a23753 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -95,6 +95,17 @@ class UserBackendService implements IUserBackendService { return UserEventPasscodeSignIn(payload).send(); } + Future> signInWithPassword( + String email, + String password, + ) { + final payload = SignInPayloadPB( + email: email, + password: password, + ); + return UserEventSignInWithEmailPassword(payload).send(); + } + static Future> signOut() { return UserEventSignOut().send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index 8034dccd32..349dd7e0d4 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -2,6 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -50,11 +52,6 @@ class _ContinueWithEmailAndPasswordState ); } else if (successOrFail == null && !state.isSubmitting) { emailKey.currentState?.clearError(); - - // _pushContinueWithMagicLinkOrPasscodePage( - // context, - // controller.text, - // ); } }, child: Column( @@ -76,13 +73,24 @@ class _ContinueWithEmailAndPasswordState controller.text, ), ), - // VSpace(theme.spacing.l), - // ContinueWithPassword( - // onTap: () => _pushContinueWithPasswordPage( - // context, - // controller.text, - // ), - // ), + VSpace(theme.spacing.l), + ContinueWithPassword( + onTap: () { + final email = controller.text; + + if (!isEmail(email)) { + emailKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidEmail.tr(), + ); + return; + } + + _pushContinueWithPasswordPage( + context, + email, + ); + }, + ), ], ), ); @@ -147,31 +155,34 @@ class _ContinueWithEmailAndPasswordState _hasPushedContinueWithMagicLinkOrPasscodePage = true; } - // void _pushContinueWithPasswordPage( - // BuildContext context, - // String email, - // ) { - // final signInBloc = context.read(); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => BlocProvider.value( - // value: signInBloc, - // child: ContinueWithPasswordPage( - // email: email, - // backToLogin: () => Navigator.pop(context), - // onEnterPassword: (password) => signInBloc.add( - // SignInEvent.signInWithEmailAndPassword( - // email: email, - // password: password, - // ), - // ), - // onForgotPassword: () { - // // todo: implement forgot password - // }, - // ), - // ), - // ), - // ); - // } + void _pushContinueWithPasswordPage( + BuildContext context, + String email, + ) { + final signInBloc = context.read(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: signInBloc, + child: ContinueWithPasswordPage( + email: email, + backToLogin: () { + emailKey.currentState?.clearError(); + Navigator.pop(context); + }, + onEnterPassword: (password) => signInBloc.add( + SignInEvent.signInWithEmailAndPassword( + email: email, + password: password, + ), + ), + onForgotPassword: () { + // todo: implement forgot password + }, + ), + ), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index 5be30ef84c..8cfc4c1157 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -167,7 +167,7 @@ class _ContinueWithMagicLinkOrPasscodePageState // title Text( LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), @@ -199,7 +199,7 @@ class _ContinueWithMagicLinkOrPasscodePageState // title Text( LocaleKeys.signIn_enterCode.tr(), - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart index 4ab40011d2..1e2ed6e100 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart @@ -1,6 +1,9 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; @@ -43,9 +46,16 @@ class _ContinueWithPasswordPageState extends State { width: 320, child: BlocListener( listener: (context, state) { - if (state.passwordError != null) { + final successOrFail = state.successOrFail; + if (successOrFail != null && successOrFail.isFailure) { + successOrFail.onFailure((error) { + inputPasswordKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), + ); + }); + } else if (state.passwordError != null) { inputPasswordKey.currentState?.syncError( - errorText: 'Incorrect password. Please try again.', + errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), ); } else { inputPasswordKey.currentState?.clearError(); @@ -80,8 +90,8 @@ class _ContinueWithPasswordPageState extends State { // title Text( - 'Enter password', - style: theme.textStyle.heading.h3( + LocaleKeys.signIn_enterPassword.tr(), + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), @@ -92,13 +102,13 @@ class _ContinueWithPasswordPageState extends State { text: TextSpan( children: [ TextSpan( - text: 'Login as ', + text: LocaleKeys.signIn_loginAs.tr(), style: theme.textStyle.body.standard( color: theme.textColorScheme.primary, ), ), TextSpan( - text: widget.email, + text: ' ${widget.email}', style: theme.textStyle.body.enhanced( color: theme.textColorScheme.primary, ), @@ -111,13 +121,26 @@ class _ContinueWithPasswordPageState extends State { } List _buildPasswordSection() { + final theme = AppFlowyTheme.of(context); + final iconSize = 20.0; return [ // Password input AFTextField( key: inputPasswordKey, controller: passwordController, - hintText: 'Enter password', + hintText: LocaleKeys.signIn_enterPassword.tr(), autoFocus: true, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + inputPasswordKey.currentState?.syncObscured(!isObscured); + }, + ), onSubmitted: widget.onEnterPassword, ), // todo: ask designer to provide the spacing @@ -127,7 +150,7 @@ class _ContinueWithPasswordPageState extends State { Align( alignment: Alignment.centerLeft, child: AFGhostTextButton( - text: 'Forget password?', + text: LocaleKeys.signIn_forgotPassword.tr(), size: AFButtonSize.s, padding: EdgeInsets.zero, onTap: widget.onForgotPassword, @@ -144,7 +167,7 @@ class _ContinueWithPasswordPageState extends State { // Continue button AFFilledTextButton.primary( - text: 'Continue', + text: LocaleKeys.web_continue.tr(), onTap: () => widget.onEnterPassword(passwordController.text), size: AFButtonSize.l, alignment: Alignment.center, @@ -156,7 +179,7 @@ class _ContinueWithPasswordPageState extends State { List _buildBackToLogin() { return [ AFGhostTextButton( - text: 'Back to Login', + text: LocaleKeys.signIn_backToLogin.tr(), size: AFButtonSize.s, onTap: widget.backToLogin, padding: EdgeInsets.zero, diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart index 93ccea25d0..14b1c896a9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart @@ -25,7 +25,7 @@ class FlowyLogoTitle extends StatelessWidget { const VSpace(20), Text( title, - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 56faa9f8d8..2f62177661 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -54,17 +54,27 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - removeUserIcon: () { - // Empty Icon URL = No icon - _userService.updateUserProfile(iconUrl: "").then((result) { + updateUserEmail: (String email) { + _userService.updateUserProfile(email: email).then((result) { result.fold( (l) => null, (err) => Log.error(err), ); }); }, - updateUserEmail: (String email) { - _userService.updateUserProfile(email: email).then((result) { + updateUserPassword: (String oldPassword, String newPassword) { + _userService + .updateUserProfile(password: newPassword) + .then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + removeUserIcon: () { + // Empty Icon URL = No icon + _userService.updateUserProfile(iconUrl: "").then((result) { result.fold( (l) => null, (err) => Log.error(err), @@ -104,10 +114,19 @@ class SettingsUserViewBloc extends Bloc { @freezed class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; - const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; - const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail; - const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) = - _UpdateUserIcon; + const factory SettingsUserEvent.updateUserName({ + required String name, + }) = _UpdateUserName; + const factory SettingsUserEvent.updateUserEmail({ + required String email, + }) = _UpdateEmail; + const factory SettingsUserEvent.updateUserIcon({ + required String iconUrl, + }) = _UpdateUserIcon; + const factory SettingsUserEvent.updateUserPassword({ + required String oldPassword, + required String newPassword, + }) = _UpdateUserPassword; const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart index 5e1a6f90e0..05e6d46957 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart @@ -105,9 +105,9 @@ class SidebarToast extends StatelessWidget { if (role.isOwner) { showSettingsDialog( context, - userProfile, - userWorkspaceBloc, - SettingsPage.plan, + userProfile: userProfile, + userWorkspaceBloc: userWorkspaceBloc, + initPage: SettingsPage.plan, ); } else { final String message; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 84a76cfe83..0bd5dafe91 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; @@ -33,7 +34,7 @@ HotKeyItem openSettingsHotKey( ), keyDownHandler: (_) { if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context, userProfile); + showSettingsDialog(context, userProfile: userProfile); } else { Navigator.of(context, rootNavigator: true) .popUntil((route) => route.isFirst); @@ -57,37 +58,55 @@ class UserSettingButton extends StatefulWidget { class _UserSettingButtonState extends State { late UserWorkspaceBloc _userWorkspaceBloc; + late PasswordBloc _passwordBloc; @override void initState() { super.initState(); + _userWorkspaceBloc = context.read(); + _passwordBloc = PasswordBloc(widget.userProfile) + ..add(PasswordEvent.init()) + ..add(PasswordEvent.checkHasPassword()); } @override void didChangeDependencies() { _userWorkspaceBloc = context.read(); + super.didChangeDependencies(); } + @override + void dispose() { + _passwordBloc.close(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return SizedBox.square( dimension: 24.0, child: FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), - child: FlowyButton( - onTap: () => showSettingsDialog( - context, - widget.userProfile, - _userWorkspaceBloc, - ), - margin: EdgeInsets.zero, - text: FlowySvg( - FlowySvgs.settings_s, - color: - widget.isHover ? Theme.of(context).colorScheme.onSurface : null, - opacity: 0.7, + child: BlocProvider.value( + value: _passwordBloc, + child: FlowyButton( + onTap: () => showSettingsDialog( + context, + userProfile: widget.userProfile, + userWorkspaceBloc: _userWorkspaceBloc, + passwordBloc: _passwordBloc, + ), + margin: EdgeInsets.zero, + text: FlowySvg( + FlowySvgs.settings_s, + color: widget.isHover + ? Theme.of(context).colorScheme.onSurface + : null, + opacity: 0.7, + ), ), ), ), @@ -96,21 +115,33 @@ class _UserSettingButtonState extends State { } void showSettingsDialog( - BuildContext context, - UserProfilePB userProfile, [ - UserWorkspaceBloc? bloc, + BuildContext context, { + required UserProfilePB userProfile, + UserWorkspaceBloc? userWorkspaceBloc, + PasswordBloc? passwordBloc, SettingsPage? initPage, -]) { +}) { AFFocusManager.maybeOf(context)?.notifyLoseFocus(); showDialog( context: context, builder: (dialogContext) => MultiBlocProvider( key: _settingsDialogKey, providers: [ + passwordBloc != null + ? BlocProvider.value( + value: passwordBloc, + ) + : BlocProvider( + create: (context) => PasswordBloc(userProfile) + ..add(PasswordEvent.init()) + ..add(PasswordEvent.checkHasPassword()), + ), BlocProvider.value( value: BlocProvider.of(dialogContext), ), - BlocProvider.value(value: bloc ?? context.read()), + BlocProvider.value( + value: userWorkspaceBloc ?? context.read(), + ), ], child: SettingsDialog( userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart index 71dfdde7a9..2125ea4b66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -16,28 +17,29 @@ class SettingsAppVersion extends StatelessWidget { Widget build(BuildContext context) { return ApplicationInfo.isUpdateAvailable ? const _UpdateAppSection() - : _buildIsUpToDate(); + : _buildIsUpToDate(context); } - Widget _buildIsUpToDate() { + Widget _buildIsUpToDate(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.regular( + Text( LocaleKeys.settings_accountPage_isUpToDate.tr(), - figmaLineHeight: 17, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), ), const VSpace(4), - Opacity( - opacity: 0.7, - child: FlowyText.regular( - LocaleKeys.settings_accountPage_officialVersion.tr( - namedArgs: { - 'version': ApplicationInfo.applicationVersion, - }, - ), - fontSize: 12, - figmaLineHeight: 13, + Text( + LocaleKeys.settings_accountPage_officialVersion.tr( + namedArgs: { + 'version': ApplicationInfo.applicationVersion, + }, + ), + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index e6c011156b..04d078ec0d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -8,8 +8,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -43,43 +43,36 @@ class _AccountDeletionButtonState extends State { @override Widget build(BuildContext context) { - final textColor = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF4F4F4F) - : const Color(0xFFB0B0B0); + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText( + Text( LocaleKeys.button_deleteAccount.tr(), - fontSize: 14.0, - fontWeight: FontWeight.w500, - figmaLineHeight: 21.0, - color: textColor, + style: theme.textStyle.heading4.enhanced( + color: theme.textColorScheme.primary, + ), ), const VSpace(8), Row( children: [ Expanded( - child: FlowyText.regular( + child: Text( LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(), - fontSize: 12.0, - figmaLineHeight: 13.0, maxLines: 2, - color: textColor, + overflow: TextOverflow.ellipsis, + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, + ), ), ), - FlowyTextButton( - LocaleKeys.button_deleteAccount.tr(), - constraints: const BoxConstraints(minHeight: 32), - padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10), - fillColor: Colors.transparent, - radius: Corners.s8Border, - hoverColor: - Theme.of(context).colorScheme.error.withValues(alpha: 0.1), - fontColor: Theme.of(context).colorScheme.error, - fontSize: 12, - isDangerous: true, - onPressed: () { + AFOutlinedTextButton.destructive( + text: LocaleKeys.button_deleteAccount.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.error, + weight: FontWeight.w400, + ), + onTap: () { isCheckedNotifier.value = false; textEditingController.clear(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 984598f29c..89066ea649 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -3,12 +3,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/user/application/prelude.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart'; import 'package:appflowy/util/navigator_context_extension.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -28,9 +32,15 @@ class AccountSignInOutSection extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Row( children: [ - FlowyText.regular(LocaleKeys.settings_accountPage_login_title.tr()), + Text( + LocaleKeys.settings_accountPage_login_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), const Spacer(), AccountSignInOutButton( userProfile: userProfile, @@ -56,13 +66,10 @@ class AccountSignInOutButton extends StatelessWidget { @override Widget build(BuildContext context) { - return PrimaryRoundedButton( + return AFFilledTextButton.primary( text: signIn ? LocaleKeys.settings_accountPage_login_loginLabel.tr() : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - fontWeight: FontWeight.w500, - radius: 8.0, onTap: () => signIn ? _showSignInDialog(context) : _showLogoutDialog(context), ); @@ -96,6 +103,94 @@ class AccountSignInOutButton extends StatelessWidget { } } +class ChangePasswordSection extends StatelessWidget { + const ChangePasswordSection({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + Text( + LocaleKeys.newSettings_myAccount_password_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + state.hasPassword + ? AFFilledTextButton.primary( + text: LocaleKeys + .newSettings_myAccount_password_changePassword + .tr(), + onTap: () => _showChangePasswordDialog(context), + ) + : AFFilledTextButton.primary( + text: LocaleKeys + .newSettings_myAccount_password_setupPassword + .tr(), + onTap: () => _showSetPasswordDialog(context), + ), + ], + ); + }, + ); + } + + Future _showChangePasswordDialog(BuildContext context) async { + final theme = AppFlowyTheme.of(context); + await showDialog( + context: context, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: getIt(), + ), + ], + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + ), + child: ChangePasswordDialogContent( + userProfile: userProfile, + ), + ), + ), + ); + } + + Future _showSetPasswordDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: getIt(), + ), + ], + child: Dialog( + child: SetupPasswordDialogContent( + userProfile: userProfile, + ), + ), + ), + ); + } +} + class _SignInDialogContent extends StatelessWidget { const _SignInDialogContent(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart index bd08501ae4..62a6232c4a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart @@ -4,6 +4,7 @@ import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_ui/appflowy_ui.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'; @@ -96,27 +97,29 @@ class _AccountUserProfileState extends State { } Widget _buildNameDisplay() { + final theme = AppFlowyTheme.of(context); return Padding( padding: const EdgeInsets.only(top: 12), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: FlowyText.medium( + child: Text( widget.name, overflow: TextOverflow.ellipsis, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), ), ), const HSpace(4), - GestureDetector( - behavior: HitTestBehavior.opaque, + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), onTap: () => setState(() => isEditing = true), - child: const FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: EdgeInsets.all(4), - child: FlowySvg(FlowySvgs.edit_s), - ), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.toolbar_link_edit_m, + size: const Size.square(20), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart new file mode 100644 index 0000000000..d606f870ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart @@ -0,0 +1,38 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/widgets.dart'; + +class SettingsEmailSection extends StatelessWidget { + const SettingsEmailSection({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.settings_accountPage_email_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + VSpace(theme.spacing.s), + Text( + userProfile.email, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart new file mode 100644 index 0000000000..194254c869 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart @@ -0,0 +1,330 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChangePasswordDialogContent extends StatefulWidget { + const ChangePasswordDialogContent({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => + _ChangePasswordDialogContentState(); +} + +class _ChangePasswordDialogContentState + extends State { + final currentPasswordTextFieldKey = GlobalKey(); + final newPasswordTextFieldKey = GlobalKey(); + final confirmPasswordTextFieldKey = GlobalKey(); + + final currentPasswordController = TextEditingController(); + final newPasswordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + final iconSize = 20.0; + + @override + void dispose() { + currentPasswordController.dispose(); + newPasswordController.dispose(); + confirmPasswordController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocListener( + listener: _onPasswordStateChanged, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(context), + VSpace(theme.spacing.l), + ..._buildCurrentPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildNewPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildConfirmPasswordFields(context), + VSpace(theme.spacing.l), + _buildSubmitButton(context), + ], + ), + ), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Change password', + style: theme.textStyle.heading4.prominent( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.password_close_m, + size: const Size.square(20), + ), + ), + ], + ); + } + + List _buildCurrentPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_currentPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: currentPasswordTextFieldKey, + controller: currentPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_enterYourCurrentPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildNewPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_newPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: newPasswordTextFieldKey, + controller: newPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_enterYourNewPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + newPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildConfirmPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: confirmPasswordTextFieldKey, + controller: confirmPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_confirmYourNewPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + Widget _buildSubmitButton(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AFOutlinedTextButton.normal( + text: LocaleKeys.button_cancel.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + weight: FontWeight.w400, + ), + onTap: () => Navigator.of(context).pop(), + ), + const HSpace(16), + AFFilledTextButton.primary( + text: LocaleKeys.button_save.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.onFill, + weight: FontWeight.w400, + ), + onTap: () => _save(context), + ), + ], + ); + } + + void _save(BuildContext context) async { + _resetError(); + + final currentPassword = currentPasswordController.text; + final newPassword = newPasswordController.text; + final confirmPassword = confirmPasswordController.text; + + if (newPassword.isEmpty) { + newPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsRequired + .tr(), + ); + return; + } + + if (confirmPassword.isEmpty) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_confirmPasswordIsRequired + .tr(), + ); + return; + } + + if (newPassword != confirmPassword) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_passwordsDoNotMatch + .tr(), + ); + return; + } + + if (newPassword == currentPassword) { + newPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsSameAsCurrent + .tr(), + ); + return; + } + + // all the verification passed, save the new password + context.read().add( + PasswordEvent.changePassword( + oldPassword: currentPassword, + newPassword: newPassword, + ), + ); + } + + void _resetError() { + currentPasswordTextFieldKey.currentState?.clearError(); + newPasswordTextFieldKey.currentState?.clearError(); + confirmPasswordTextFieldKey.currentState?.clearError(); + } + + void _onPasswordStateChanged(BuildContext context, PasswordState state) { + bool hasError = false; + String message = ''; + String description = ''; + + final changePasswordResult = state.changePasswordResult; + final setPasswordResult = state.setupPasswordResult; + + if (changePasswordResult != null) { + changePasswordResult.fold( + (success) { + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordUpdatedSuccessfully + .tr(); + }, + (error) { + hasError = true; + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordUpdatedFailed + .tr(); + description = error.msg; + }, + ); + } else if (setPasswordResult != null) { + setPasswordResult.fold( + (success) { + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordSetupSuccessfully + .tr(); + }, + (error) { + hasError = true; + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordSetupFailed + .tr(); + description = error.msg; + }, + ); + } + + if (!state.isSubmitting && message.isNotEmpty) { + showToastNotification( + message: message, + description: description, + type: hasError ? ToastificationType.error : ToastificationType.success, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart new file mode 100644 index 0000000000..5417b1a0eb --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart @@ -0,0 +1,30 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class PasswordSuffixIcon extends StatelessWidget { + const PasswordSuffixIcon({ + super.key, + required this.isObscured, + required this.onTap, + }); + + final bool isObscured; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Padding( + padding: EdgeInsets.only(right: theme.spacing.m), + child: GestureDetector( + onTap: onTap, + child: FlowySvg( + isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s, + color: theme.textColorScheme.secondary, + size: const Size.square(20), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart new file mode 100644 index 0000000000..2fdfd8b934 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart @@ -0,0 +1,254 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SetupPasswordDialogContent extends StatefulWidget { + const SetupPasswordDialogContent({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => + _SetupPasswordDialogContentState(); +} + +class _SetupPasswordDialogContentState + extends State { + final passwordTextFieldKey = GlobalKey(); + final confirmPasswordTextFieldKey = GlobalKey(); + + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + final iconSize = 20.0; + + @override + void dispose() { + passwordController.dispose(); + confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocListener( + listener: _onPasswordStateChanged, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(context), + VSpace(theme.spacing.l), + ..._buildPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildConfirmPasswordFields(context), + VSpace(theme.spacing.l), + _buildSubmitButton(context), + ], + ), + ), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.newSettings_myAccount_password_setupPassword.tr(), + style: theme.textStyle.heading4.prominent( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.password_close_m, + size: const Size.square(20), + ), + ), + ], + ); + } + + List _buildPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + 'Password', + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: passwordTextFieldKey, + controller: passwordController, + hintText: 'Enter your password', + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + passwordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildConfirmPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + 'Confirm password', + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: confirmPasswordTextFieldKey, + controller: confirmPasswordController, + hintText: 'Confirm your password', + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + Widget _buildSubmitButton(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AFOutlinedTextButton.normal( + text: 'Cancel', + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + weight: FontWeight.w400, + ), + onTap: () => Navigator.of(context).pop(), + ), + const HSpace(16), + AFFilledTextButton.primary( + text: 'Save', + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.onFill, + weight: FontWeight.w400, + ), + onTap: () => _save(context), + ), + ], + ); + } + + void _save(BuildContext context) async { + _resetError(); + + final password = passwordController.text; + final confirmPassword = confirmPasswordController.text; + + if (password.isEmpty) { + passwordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsRequired + .tr(), + ); + return; + } + + if (confirmPassword.isEmpty) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_confirmPasswordIsRequired + .tr(), + ); + return; + } + + if (password != confirmPassword) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_passwordsDoNotMatch + .tr(), + ); + return; + } + + // all the verification passed, save the password + context.read().add( + PasswordEvent.setupPassword( + newPassword: password, + ), + ); + } + + void _resetError() { + passwordTextFieldKey.currentState?.clearError(); + confirmPasswordTextFieldKey.currentState?.clearError(); + } + + void _onPasswordStateChanged(BuildContext context, PasswordState state) { + bool hasError = false; + String message = ''; + String description = ''; + + final setPasswordResult = state.setupPasswordResult; + + if (setPasswordResult != null) { + setPasswordResult.fold( + (success) { + message = 'Password set'; + description = 'Your password has been set'; + }, + (error) { + hasError = true; + message = 'Failed to set password'; + description = error.msg; + }, + ); + } + + if (!state.isSubmitting && message.isNotEmpty) { + showToastNotification( + message: message, + description: description, + type: hasError ? ToastificationType.error : ToastificationType.success, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index 701d1cb565..e223b2d063 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -4,12 +4,12 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart'; import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -45,11 +45,11 @@ class _SettingsAccountViewState extends State { child: BlocBuilder( builder: (context, state) { return SettingsBody( - title: LocaleKeys.settings_accountPage_title.tr(), + title: LocaleKeys.newSettings_myAccount_title.tr(), children: [ // user profile SettingsCategory( - title: LocaleKeys.settings_accountPage_general_title.tr(), + title: LocaleKeys.newSettings_myAccount_myProfile.tr(), children: [ AccountUserProfile( name: userName, @@ -61,7 +61,7 @@ class _SettingsAccountViewState extends State { setState(() => userName = newName); context .read() - .add(SettingsUserEvent.updateUserName(newName)); + .add(SettingsUserEvent.updateUserName(name: newName)); }, ), ], @@ -72,9 +72,14 @@ class _SettingsAccountViewState extends State { if (isAuthEnabled && state.userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( - title: LocaleKeys.settings_accountPage_email_title.tr(), + title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ - FlowyText.regular(state.userProfile.email), + SettingsEmailSection( + userProfile: state.userProfile, + ), + ChangePasswordSection( + userProfile: state.userProfile, + ), AccountSignInOutSection( userProfile: state.userProfile, onAction: state.userProfile.authenticator == diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index a111fa2626..33c81b99e8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -25,15 +26,18 @@ class SettingsCategory extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - FlowyText.semibold( + Text( title, + style: theme.textStyle.heading4.enhanced( + color: theme.textColorScheme.primary, + ), maxLines: 2, - fontSize: 16, overflow: TextOverflow.ellipsis, ), if (tooltip != null) ...[ @@ -47,7 +51,7 @@ class SettingsCategory extends StatelessWidget { if (actions != null) ...actions!, ], ), - const VSpace(8), + const VSpace(16), if (description?.isNotEmpty ?? false) ...[ FlowyText.regular( description!, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index 5637fdd20c..deec09c1d8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; /// This is used to create a uniform space and divider @@ -7,6 +8,11 @@ class SettingsCategorySpacer extends StatelessWidget { const SettingsCategorySpacer({super.key}); @override - Widget build(BuildContext context) => - const Divider(height: 32, color: Color(0xFFF2F2F2)); + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Divider( + height: 32, + color: theme.borderColorScheme.greyPrimary, + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart index c028e6886d..7409070ba9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; /// Renders a simple header for the settings view /// @@ -13,10 +13,16 @@ class SettingsHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.semibold(title, fontSize: 24), + Text( + title, + style: theme.textStyle.heading2.enhanced( + color: theme.textColorScheme.primary, + ), + ), if (description?.isNotEmpty == true) ...[ const VSpace(8), FlowyText( diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart index dc85a3ee55..39d5175af1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart @@ -27,7 +27,7 @@ enum AFButtonSize { vertical: theme.spacing.xs, ), AFButtonSize.m => EdgeInsets.symmetric( - horizontal: theme.spacing.l, + horizontal: theme.spacing.xl, vertical: theme.spacing.s, ), AFButtonSize.l => EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart index ced936cf0a..035307d10b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart @@ -13,6 +13,7 @@ class AFBaseTextButton extends StatelessWidget { this.textColor, this.backgroundColor, this.alignment, + this.textStyle, }); /// The text of the button. @@ -44,6 +45,9 @@ class AFBaseTextButton extends StatelessWidget { /// If it's null, the button size will be the size of the text with padding. final Alignment? alignment; + /// The text style of the button. + final TextStyle? textStyle; + @override Widget build(BuildContext context) { throw UnimplementedError(); diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart index 353d5ac785..889ad1e429 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -14,6 +14,7 @@ class AFFilledTextButton extends AFBaseTextButton { super.borderRadius, super.disabled = false, super.alignment, + super.textStyle, }); /// Primary text button. @@ -26,6 +27,7 @@ class AFFilledTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -36,6 +38,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.onFill, backgroundColor: (context, isHovering, disabled) { @@ -60,6 +63,7 @@ class AFFilledTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -70,6 +74,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.onFill, backgroundColor: (context, isHovering, disabled) { @@ -92,6 +97,7 @@ class AFFilledTextButton extends AFBaseTextButton { EdgeInsetsGeometry? padding, double? borderRadius, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -102,6 +108,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: true, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.tertiary, backgroundColor: (context, isHovering, disabled) => @@ -123,7 +130,8 @@ class AFFilledTextButton extends AFBaseTextButton { AppFlowyTheme.of(context).textColorScheme.onFill; Widget child = Text( text, - style: size.buildTextStyle(context).copyWith(color: textColor), + style: textStyle ?? + size.buildTextStyle(context).copyWith(color: textColor), ); final alignment = this.alignment; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart index 7cb5f2d609..d5ae580583 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -8,6 +8,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { required super.text, required super.onTap, this.borderColor, + super.textStyle, super.textColor, super.backgroundColor, super.size = AFButtonSize.m, @@ -27,6 +28,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -37,6 +39,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, borderColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (disabled) { @@ -80,6 +83,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -90,6 +94,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, borderColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (disabled) { @@ -127,6 +132,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { EdgeInsetsGeometry? padding, double? borderRadius, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -137,6 +143,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: true, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); return disabled @@ -185,7 +192,8 @@ class AFOutlinedTextButton extends AFBaseTextButton { Widget child = Text( text, - style: size.buildTextStyle(context).copyWith(color: textColor), + style: textStyle ?? + size.buildTextStyle(context).copyWith(color: textColor), ); final alignment = this.alignment; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 595d4bb859..a934734983 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -6,8 +6,12 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function( ); abstract class AFTextFieldState extends State { + // Error handler void syncError({required String errorText}) {} void clearError() {} + + /// Obscure the text. + void syncObscured(bool isObscured) {} } class AFTextField extends StatefulWidget { @@ -23,6 +27,9 @@ class AFTextField extends StatefulWidget { this.onSubmitted, this.autoFocus, this.height = 40.0, + this.obscureText = false, + this.suffixIconBuilder, + this.suffixIconConstraints, }); /// The height of the text field. @@ -57,6 +64,16 @@ class AFTextField extends StatefulWidget { /// Enable auto focus. final bool? autoFocus; + /// Obscure the text. + final bool obscureText; + + /// The trailing widget to display. + final Widget Function(BuildContext context, bool isObscured)? + suffixIconBuilder; + + /// The size of the suffix icon. + final BoxConstraints? suffixIconConstraints; + @override State createState() => _AFTextFieldState(); } @@ -67,6 +84,8 @@ class _AFTextFieldState extends AFTextFieldState { bool hasError = false; String errorText = ''; + bool isObscured = false; + @override void initState() { super.initState(); @@ -79,6 +98,8 @@ class _AFTextFieldState extends AFTextFieldState { } effectiveController.addListener(_validate); + + isObscured = widget.obscureText; } @override @@ -107,6 +128,7 @@ class _AFTextFieldState extends AFTextFieldState { style: theme.textStyle.body.standard( color: theme.textColorScheme.primary, ), + obscureText: isObscured, onChanged: widget.onChanged, onSubmitted: widget.onSubmitted, autofocus: widget.autoFocus ?? false, @@ -152,6 +174,8 @@ class _AFTextFieldState extends AFTextFieldState { borderRadius: borderRadius, ), hoverColor: theme.borderColorScheme.greyTertiaryHover, + suffixIcon: widget.suffixIconBuilder?.call(context, isObscured), + suffixIconConstraints: widget.suffixIconConstraints, ), ); @@ -204,4 +228,11 @@ class _AFTextFieldState extends AFTextFieldState { errorText = ''; }); } + + @override + void syncObscured(bool isObscured) { + setState(() { + this.isObscured = isObscured; + }); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart index a85ffbb6cb..3cdf267fe0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart @@ -6,66 +6,86 @@ abstract class TextThemeType { TextStyle standard({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle enhanced({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle prominent({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle underline({ String family = '', Color? color, + FontWeight? weight, }); } -class TextThemeHeading { - const TextThemeHeading(); +class TextThemeHeading1 extends TextThemeType { + const TextThemeHeading1(); - TextStyle h1({ + @override + TextStyle standard({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, fontSize: 36, height: 40 / 36, color: color, + weight: weight ?? FontWeight.w400, ); - TextStyle h2({ + @override + TextStyle enhanced({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 24, - height: 32 / 24, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.w600, ); - TextStyle h3({ + @override + TextStyle prominent({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 20, - height: 28 / 20, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.w700, ); - TextStyle h4({ + @override + TextStyle underline({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 16, - height: 22 / 16, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.bold, + decoration: TextDecoration.underline, ); static TextStyle _defaultTextStyle({ @@ -74,13 +94,188 @@ class TextThemeHeading { required double height, TextDecoration decoration = TextDecoration.none, Color? color, + FontWeight weight = FontWeight.bold, }) => TextStyle( inherit: false, fontSize: fontSize, decoration: decoration, fontStyle: FontStyle.normal, - fontWeight: FontWeight.bold, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading2 extends TextThemeType { + const TextThemeHeading2(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 24, + double height = 32 / 24, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading3 extends TextThemeType { + const TextThemeHeading3(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 20, + double height = 28 / 20, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading4 extends TextThemeType { + const TextThemeHeading4(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 16, + double height = 22 / 16, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, height: height, fontFamily: family, color: color, @@ -93,29 +288,35 @@ class TextThemeHeadline extends TextThemeType { const TextThemeHeadline(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -123,8 +324,8 @@ class TextThemeHeadline extends TextThemeType { required String family, double fontSize = 24, double height = 36 / 24, - FontWeight weight = FontWeight.normal, TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.normal, Color? color, }) => TextStyle( @@ -145,29 +346,35 @@ class TextThemeTitle extends TextThemeType { const TextThemeTitle(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -197,29 +404,35 @@ class TextThemeBody extends TextThemeType { const TextThemeBody(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -249,29 +462,35 @@ class TextThemeCaption extends TextThemeType { const TextThemeCaption(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart index 64daae2370..1caaa288e8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart @@ -2,14 +2,20 @@ import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart'; class AppFlowyBaseTextStyle { const AppFlowyBaseTextStyle({ - this.heading = const TextThemeHeading(), + this.heading1 = const TextThemeHeading1(), + this.heading2 = const TextThemeHeading2(), + this.heading3 = const TextThemeHeading3(), + this.heading4 = const TextThemeHeading4(), this.headline = const TextThemeHeadline(), this.title = const TextThemeTitle(), this.body = const TextThemeBody(), this.caption = const TextThemeCaption(), }); - final TextThemeHeading heading; + final TextThemeType heading1; + final TextThemeType heading2; + final TextThemeType heading3; + final TextThemeType heading4; final TextThemeType headline; final TextThemeType title; final TextThemeType body; diff --git a/frontend/resources/flowy_icons/20x/hide_password.svg b/frontend/resources/flowy_icons/20x/hide_password.svg new file mode 100644 index 0000000000..2ebd274866 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/hide_password.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/20x/password_close.svg b/frontend/resources/flowy_icons/20x/password_close.svg new file mode 100644 index 0000000000..52a44e1a8e --- /dev/null +++ b/frontend/resources/flowy_icons/20x/password_close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/20x/show_password.svg b/frontend/resources/flowy_icons/20x/show_password.svg new file mode 100644 index 0000000000..ac8d092b37 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/show_password.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 6274540914..30e8c476ae 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -81,8 +81,11 @@ "enterCode": "Enter code", "enterCodeManually": "Enter code manually", "continueWithEmail": "Continue with email", + "enterPassword": "Enter password", + "loginAs": "Login as", "invalidVerificationCode": "Please enter a valid verification code", - "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later." + "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later.", + "invalidLoginCredentials": "Your password is incorrect, please try again" }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -2618,7 +2621,7 @@ "noLogFiles": "There're no log files", "newSettings": { "myAccount": { - "title": "My account", + "title": "Account & App", "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", "profileLabel": "Account name & Profile image", "profileNamePlaceholder": "Enter your name", @@ -2644,7 +2647,34 @@ "failedToGetCurrentUser": "Failed to get current user email", "confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", "deleteAccountSuccess": "Account deleted successfully" - } + }, + "password": { + "title": "Password", + "changePassword": "Change password", + "currentPassword": "Current password", + "newPassword": "New password", + "confirmNewPassword": "Confirm new password", + "setupPassword": "Setup password", + "error": { + "newPasswordIsRequired": "New password is required", + "confirmPasswordIsRequired": "Confirm password is required", + "passwordsDoNotMatch": "Passwords do not match", + "newPasswordIsSameAsCurrent": "New password is same as current password" + }, + "toast": { + "passwordUpdatedSuccessfully": "Password updated successfully", + "passwordUpdatedFailed": "Failed to update password", + "passwordSetupSuccessfully": "Password setup successfully", + "passwordSetupFailed": "Failed to setup password" + }, + "hint": { + "enterYourCurrentPassword": "Enter your current password", + "enterYourNewPassword": "Enter your new password", + "confirmYourNewPassword": "Confirm your new password" + } + }, + "myAccount": "My Account", + "myProfile": "My Profile" }, "workplace": { "name": "Workplace", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ec114f205a..b2bb89e3b9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -5148,7 +5148,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5168,6 +5168,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5235,6 +5236,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 3cd3733837..3c73fd01eb 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -31,29 +31,6 @@ async fn sign_up_with_invalid_email() { ); } } -#[tokio::test] -async fn sign_up_with_long_password() { - let sdk = EventIntegrationTest::new().await; - let request = SignUpPayloadPB { - email: unique_email(), - name: valid_name(), - password: "1234".repeat(100).as_str().to_string(), - auth_type: AuthenticatorPB::Local, - device_id: "".to_string(), - }; - - assert_eq!( - EventBuilder::new(sdk) - .event(SignUp) - .payload(request) - .async_send() - .await - .error() - .unwrap() - .code, - ErrorCode::PasswordTooLong - ); -} #[tokio::test] async fn sign_in_with_invalid_email() { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 9057288f88..98ed822901 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -131,12 +131,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), ), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } @@ -247,12 +245,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 02ae333aa0..87e828199c 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -31,11 +31,10 @@ impl TryInto for SignInPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; - let password = UserPassword::parse(self.password)?; Ok(SignInParams { email: email.0, - password: password.0, + password: self.password, name: self.name, auth_type: self.auth_type.into(), }) @@ -65,13 +64,13 @@ impl TryInto for SignUpPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; - let password = UserPassword::parse(self.password)?; + let password = self.password; let name = UserName::parse(self.name)?; Ok(SignUpParams { email: email.0, name: name.0, - password: password.0, + password, auth_type: self.auth_type.into(), device_id: self.device_id, }) diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 36d5232bd2..7e62958c68 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -4,7 +4,7 @@ use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; +use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey}; use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; @@ -171,10 +171,7 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(email) => Some(UserEmail::parse(email)?.0), }; - let password = match self.password { - None => None, - Some(password) => Some(UserPassword::parse(password)?.0), - }; + let password = self.password; let icon_url = match self.icon_url { None => None, From 4925e991660a5a6d89ada9705473051553e06f8b Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:02:02 +0800 Subject: [PATCH 353/384] chore: fix compile --- .../ai_chat/application/chat_bloc.dart | 2 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 11 +- frontend/rust-lib/flowy-ai/src/entities.rs | 9 +- .../rust-lib/flowy-ai/src/event_handler.rs | 33 +----- .../flowy-ai/src/local_ai/controller.rs | 107 +++++++++--------- .../src/middleware/chat_service_mw.rs | 41 +------ .../rust-lib/flowy-ai/src/stream_message.rs | 1 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 22 ++-- 9 files changed, 78 insertions(+), 152 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 4924a42c0d..602b46f97a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -435,7 +435,7 @@ class ChatBloc extends Bloc { messageType: ChatMessageTypePB.User, questionStreamPort: Int64(questionStream.nativePort), answerStreamPort: Int64(answerStream!.nativePort), - metadata: await metadataPBFromMetadata(metadata), + //metadata: await metadataPBFromMetadata(metadata), ); if (format != null) { payload.format = format.toPB(); diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index b91021142e..2292e0f332 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -6,8 +6,8 @@ pub use client_api::entity::ai_dto::{ }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ - ChatMessage, ChatMessageMetadata, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, - MessageCursor, RepeatedChatMessage, UpdateChatParams, + ChatMessage, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, MessageCursor, + RepeatedChatMessage, UpdateChatParams, }; pub use client_api::entity::QuestionStreamValue; pub use client_api::entity::*; diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index ba5ff431e9..3180227ed0 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -76,11 +76,10 @@ impl Chat { preferred_ai_model: Option, ) -> Result { trace!( - "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", + "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, format={:?}", self.chat_id, params.message, params.message_type, - params.metadata, params.format, ); @@ -116,14 +115,6 @@ impl Chat { .send(StreamMessage::MessageId(question.message_id).to_string()) .await; - if let Err(err) = self - .chat_service - .index_message_metadata(&self.chat_id, ¶ms.metadata, &mut question_sink) - .await - { - error!("Failed to index file: {}", err); - } - // Save message to disk notify_message(&self.chat_id, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index b62899eca3..5a4aecbbd7 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -2,9 +2,8 @@ use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; use af_plugin::core::plugin::RunningState; use flowy_ai_pub::cloud::{ - AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, - OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, - ResponseFormat, + AIModel, ChatMessage, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, + RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -71,9 +70,6 @@ pub struct StreamChatPayloadPB { #[pb(index = 6, one_of)] pub format: Option, - - #[pb(index = 7)] - pub metadata: Vec, } #[derive(Default, Debug)] @@ -84,7 +80,6 @@ pub struct StreamMessageParams { pub answer_stream_port: i64, pub question_stream_port: i64, pub format: Option, - pub metadata: Vec, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 6788685102..85f2dc8306 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -2,16 +2,13 @@ use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; use crate::completion::AICompletion; use crate::entities::*; use crate::util::ai_available_models_key; -use flowy_ai_pub::cloud::{ - AIModel, ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader, -}; +use flowy_ai_pub::cloud::{AIModel, ChatMessageType}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use std::fs; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::trace; use uuid::Uuid; use validator::Validate; @@ -37,7 +34,6 @@ pub(crate) async fn stream_chat_message_handler( answer_stream_port, question_stream_port, format, - metadata, } = data; let message_type = match message_type { @@ -45,32 +41,6 @@ pub(crate) async fn stream_chat_message_handler( ChatMessageTypePB::User => ChatMessageType::User, }; - let metadata = metadata - .into_iter() - .map(|metadata| { - let (content_type, content_len) = match metadata.loader_type { - ContextLoaderTypePB::Txt => (ContextLoader::Text, metadata.data.len()), - ContextLoaderTypePB::Markdown => (ContextLoader::Markdown, metadata.data.len()), - ContextLoaderTypePB::PDF => (ContextLoader::PDF, 0), - ContextLoaderTypePB::UnknownLoaderType => (ContextLoader::Unknown, 0), - }; - - ChatMessageMetadata { - data: ChatRAGData { - content: metadata.data, - content_type, - size: content_len as i64, - }, - id: metadata.id, - name: metadata.name.clone(), - source: metadata.source, - extra: None, - } - }) - .collect::>(); - - trace!("Stream chat message with metadata: {:?}", metadata); - let chat_id = Uuid::from_str(&chat_id)?; let params = StreamMessageParams { chat_id, @@ -79,7 +49,6 @@ pub(crate) async fn stream_chat_message_handler( answer_stream_port, question_stream_port, format, - metadata, }; let ai_manager = upgrade_ai_manager(ai_manager)?; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 3c4ccb1f9d..b9dc7a73c1 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -6,7 +6,6 @@ use crate::notification::{ }; use af_plugin::manager::PluginManager; use anyhow::Error; -use flowy_ai_pub::cloud::{ChatMessageMetadata, ContextLoader}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; @@ -21,9 +20,8 @@ use arc_swap::ArcSwapOption; use futures_util::SinkExt; use lib_infra::util::get_operating_system; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use tokio::select; use tokio_stream::StreamExt; @@ -383,58 +381,59 @@ impl LocalAIController { Ok(enabled) } - #[instrument(level = "debug", skip_all)] - pub async fn index_message_metadata( - &self, - chat_id: &Uuid, - metadata_list: &[ChatMessageMetadata], - index_process_sink: &mut (impl Sink + Unpin), - ) -> FlowyResult<()> { - if !self.is_enabled() { - info!("[AI Plugin] local ai is disabled, skip indexing"); - return Ok(()); - } - - for metadata in metadata_list { - let mut file_metadata = HashMap::new(); - file_metadata.insert("id".to_string(), json!(&metadata.id)); - file_metadata.insert("name".to_string(), json!(&metadata.name)); - file_metadata.insert("source".to_string(), json!(&metadata.source)); - - let file_path = Path::new(&metadata.data.content); - if !file_path.exists() { - return Err( - FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), - ); - } - info!( - "[AI Plugin] embed file: {:?}, with metadata: {:?}", - file_path, file_metadata - ); - - match &metadata.data.content_type { - ContextLoader::Unknown => { - error!( - "[AI Plugin] unsupported content type: {:?}", - metadata.data.content_type - ); - }, - ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { - self - .process_index_file( - chat_id, - file_path.to_path_buf(), - &file_metadata, - index_process_sink, - ) - .await?; - }, - } - } - - Ok(()) - } + // #[instrument(level = "debug", skip_all)] + // pub async fn index_message_metadata( + // &self, + // chat_id: &Uuid, + // metadata_list: &[ChatMessageMetadata], + // index_process_sink: &mut (impl Sink + Unpin), + // ) -> FlowyResult<()> { + // if !self.is_enabled() { + // info!("[AI Plugin] local ai is disabled, skip indexing"); + // return Ok(()); + // } + // + // for metadata in metadata_list { + // let mut file_metadata = HashMap::new(); + // file_metadata.insert("id".to_string(), json!(&metadata.id)); + // file_metadata.insert("name".to_string(), json!(&metadata.name)); + // file_metadata.insert("source".to_string(), json!(&metadata.source)); + // + // let file_path = Path::new(&metadata.data.content); + // if !file_path.exists() { + // return Err( + // FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), + // ); + // } + // info!( + // "[AI Plugin] embed file: {:?}, with metadata: {:?}", + // file_path, file_metadata + // ); + // + // match &metadata.data.content_type { + // ContextLoader::Unknown => { + // error!( + // "[AI Plugin] unsupported content type: {:?}", + // metadata.data.content_type + // ); + // }, + // ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { + // self + // .process_index_file( + // chat_id, + // file_path.to_path_buf(), + // &file_metadata, + // index_process_sink, + // ) + // .await?; + // }, + // } + // } + // + // Ok(()) + // } + #[allow(dead_code)] async fn process_index_file( &self, chat_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index ff5c608f22..22a2bec674 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -9,19 +9,17 @@ use flowy_ai_pub::persistence::select_message_content; use std::collections::HashMap; use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, - RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, - StreamComplete, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, + ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, + RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, + UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; -use futures::{stream, Sink, StreamExt, TryStreamExt}; +use futures::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use crate::local_ai::stream_util::QuestionStream; -use crate::stream_message::StreamMessage; use flowy_storage_pub::storage::StorageService; -use futures_util::SinkExt; use serde_json::{json, Value}; use std::path::Path; use std::sync::{Arc, Weak}; @@ -32,6 +30,7 @@ pub struct ChatServiceMiddleware { cloud_service: Arc, user_service: Arc, local_ai: Arc, + #[allow(dead_code)] storage_service: Weak, } @@ -50,34 +49,6 @@ impl ChatServiceMiddleware { } } - pub async fn index_message_metadata( - &self, - chat_id: &Uuid, - metadata_list: &[ChatMessageMetadata], - index_process_sink: &mut (impl Sink + Unpin), - ) -> Result<(), FlowyError> { - if metadata_list.is_empty() { - return Ok(()); - } - if self.local_ai.is_enabled() { - let _ = index_process_sink - .send(StreamMessage::IndexStart.to_string()) - .await; - let result = self - .local_ai - .index_message_metadata(chat_id, metadata_list, index_process_sink) - .await; - let _ = index_process_sink - .send(StreamMessage::IndexEnd.to_string()) - .await; - - result? - } else if let Some(_storage_service) = self.storage_service.upgrade() { - // - } - Ok(()) - } - fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs index 3e76282aa8..3f7b37bd34 100644 --- a/frontend/rust-lib/flowy-ai/src/stream_message.rs +++ b/frontend/rust-lib/flowy-ai/src/stream_message.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +#[allow(dead_code)] pub enum StreamMessage { MessageId(i64), IndexStart, diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f3caa86e66..91c9aa8162 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -128,15 +128,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 394ac85c3265ae4c071206dc629ea84a8b9a6ae2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:31:50 +0800 Subject: [PATCH 354/384] refactor: server type name --- .../src/deps_resolve/cloud_service_impl.rs | 24 +-- frontend/rust-lib/flowy-core/src/lib.rs | 10 +- .../rust-lib/flowy-core/src/server_layer.rs | 199 ++++++++---------- .../flowy-core/src/user_state_callback.rs | 34 ++- .../src/af_cloud/impls/user/dto.rs | 7 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 10 +- .../rust-lib/flowy-user-pub/src/entities.rs | 34 +-- .../rust-lib/flowy-user/src/entities/auth.rs | 14 +- .../rust-lib/flowy-user/src/event_handler.rs | 22 +- frontend/rust-lib/flowy-user/src/event_map.rs | 22 +- .../src/migrations/doc_key_with_workspace.rs | 4 +- .../src/migrations/document_empty_content.rs | 6 +- .../flowy-user/src/migrations/migration.rs | 6 +- .../migrations/workspace_and_favorite_v1.rs | 4 +- .../src/migrations/workspace_trash_v1.rs | 4 +- .../data_import/appflowy_data_import.rs | 4 +- .../src/services/sqlite_sql/user_sql.rs | 6 +- .../flowy-user/src/user_manager/manager.rs | 60 +++--- .../src/user_manager/manager_history_user.rs | 7 +- .../user_manager/manager_user_awareness.rs | 15 +- .../src/user_manager/user_login_state.rs | 4 +- 21 files changed, 224 insertions(+), 272 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 3a7d648133..9c1ee9462c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,4 +1,4 @@ -use crate::server_layer::{Server, ServerProvider}; +use crate::server_layer::{ServerProvider, ServerType}; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::workspace_dto::PublishInfoView; @@ -35,7 +35,7 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; -use flowy_user_pub::entities::{Authenticator, UserTokenState}; +use flowy_user_pub::entities::{AuthType, UserTokenState}; use lib_infra::async_trait::async_trait; use serde_json::Value; use std::collections::HashMap; @@ -186,18 +186,18 @@ impl UserCloudServiceProvider for ServerProvider { } } - /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. + /// When user login, the provider type is set by the [AuthType] and save to disk for next use. /// - /// Each [Authenticator] has a corresponding [Server]. The [Server] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [Server] is set, + /// Each [AuthType] has a corresponding [ServerType]. The [ServerType] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, /// it will be used when user open the app again. /// - fn set_user_authenticator(&self, authenticator: &Authenticator) { - self.set_authenticator(authenticator.clone()); + fn set_auth_type(&self, auth_type: &AuthType) { + self.set_auth_type(*auth_type); } - fn get_user_authenticator(&self) -> Authenticator { - self.get_authenticator() + fn get_auth_type(&self) -> AuthType { + self.get_auth_type() } fn set_network_reachable(&self, reachable: bool) { @@ -211,7 +211,7 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.set_secret(secret); } - /// Returns the [UserCloudService] base on the current [Server]. + /// Returns the [UserCloudService] base on the current [ServerType]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let user_service = self.get_server()?.user_service(); @@ -220,8 +220,8 @@ impl UserCloudServiceProvider for ServerProvider { fn service_url(&self) -> String { match self.get_server_type() { - Server::Local => "".to_string(), - Server::AppFlowyCloud => AFCloudConfiguration::from_env() + ServerType::Local => "".to_string(), + ServerType::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index dbbce02136..bd55c8db1d 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, Server, ServerProvider}; +use crate::server_layer::{current_server_type, ServerProvider, ServerType}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -314,11 +314,11 @@ impl AppFlowyCore { } } -impl From for CollabPluginProviderType { - fn from(server_type: Server) -> Self { +impl From for CollabPluginProviderType { + fn from(server_type: ServerType) -> Self { match server_type { - Server::Local => CollabPluginProviderType::Local, - Server::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + ServerType::Local => CollabPluginProviderType::Local, + ServerType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index a3d6de7003..c0bf043d27 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,179 +1,156 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; -use arc_swap::ArcSwapOption; +use arc_swap::{ArcSwap, ArcSwapOption}; use dashmap::DashMap; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::{AIUserServiceImpl, LoginUserService}; -use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::af_cloud::{ + define::{AIUserServiceImpl, LoginUserService}, + AppFlowyCloudServer, +}; use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; -use serde_repr::*; -use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +/// ServerType: local or cloud #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] -pub enum Server { - /// Local server provider. - /// Offline mode, no user authentication and the data is stored locally. +pub enum ServerType { Local = 0, - /// AppFlowy Cloud server provider. - /// See: https://github.com/AppFlowy-IO/AppFlowy-Cloud AppFlowyCloud = 1, } -impl Server { +impl ServerType { pub fn is_local(&self) -> bool { - matches!(self, Server::Local) + matches!(self, Self::Local) } } -impl Display for Server { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Server::Local => write!(f, "Local"), - Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), +impl Display for ServerType { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{:?}", self) + } +} + +/// Conversion between AuthType and ServerType +impl From<&AuthType> for ServerType { + fn from(a: &AuthType) -> Self { + match a { + AuthType::Local => ServerType::Local, + AuthType::AppFlowyCloud => ServerType::AppFlowyCloud, + } + } +} +impl From for AuthType { + fn from(s: ServerType) -> Self { + match s { + ServerType::Local => AuthType::Local, + ServerType::AppFlowyCloud => AuthType::AppFlowyCloud, } } } -/// The [ServerProvider] provides list of [AppFlowyServer] base on the [Authenticator]. Using -/// the auth type, the [ServerProvider] will create a new [AppFlowyServer] if it doesn't -/// exist. -/// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: DashMap>, - pub(crate) encryption: Arc, - #[allow(dead_code)] - pub(crate) store_preferences: Weak, - pub(crate) user_enable_sync: AtomicBool, - - /// The authenticator type of the user. - authenticator: AtomicU8, + providers: DashMap>, + auth_type: ArcSwap, user: Arc, - pub(crate) uid: Arc>, pub local_ai: Arc, + pub uid: Arc>, + pub user_enable_sync: Arc, + pub encryption: Arc, } impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - server: Server, + initial: ServerType, store_preferences: Weak, - server_user: impl LoginUserService + 'static, + user_service: impl LoginUserService + 'static, ) -> Self { - let user = Arc::new(server_user); - let encryption = EncryptionImpl::new(None); - let user_service = Arc::new(AIUserServiceImpl(user.clone())); - let plugin_manager = Arc::new(PluginManager::new()); + let user = Arc::new(user_service); + let initial_auth = AuthType::from(initial); + let auth_type = ArcSwap::from(Arc::new(initial_auth)); + let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; + let ai_user = Arc::new(AIUserServiceImpl(user.clone())); + let plugins = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( - plugin_manager.clone(), - store_preferences.clone(), - user_service.clone(), + plugins, + store_preferences, + ai_user.clone(), )); - Self { + ServerProvider { config, providers: DashMap::new(), - user_enable_sync: AtomicBool::new(true), - authenticator: AtomicU8::new(Authenticator::from(server) as u8), - encryption: Arc::new(encryption), - store_preferences, - uid: Default::default(), + encryption, + user_enable_sync: Arc::new(AtomicBool::new(true)), + auth_type, user, + uid: Default::default(), local_ai, } } - pub fn get_server_type(&self) -> Server { - match Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) { - Authenticator::Local => Server::Local, - Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + /// Reads current type + pub fn get_server_type(&self) -> ServerType { + let auth_type = self.auth_type.load_full(); + ServerType::from(auth_type.as_ref()) + } + + pub fn set_auth_type(&self, a: AuthType) { + let old_type = self.get_server_type(); + self.auth_type.store(Arc::new(a)); + let new_type = self.get_server_type(); + if old_type != new_type { + self.providers.remove(&old_type); } } - pub fn set_authenticator(&self, authenticator: Authenticator) { - let old_server_type = self.get_server_type(); - self - .authenticator - .store(authenticator as u8, Ordering::Release); - let new_server_type = self.get_server_type(); - - if old_server_type != new_server_type { - self.providers.remove(&old_server_type); - } + pub fn get_auth_type(&self) -> AuthType { + *self.auth_type.load_full().as_ref() } - pub fn get_authenticator(&self) -> Authenticator { - Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) - } - - /// Returns a [AppFlowyServer] trait implementation base on the provider_type. + /// Lazily create or fetch an AppFlowyServer instance pub fn get_server(&self) -> FlowyResult> { - let server_type = self.get_server_type(); - - if let Some(provider) = self.providers.get(&server_type) { - return Ok(provider.value().clone()); + let key = self.get_server_type(); + if let Some(entry) = self.providers.get(&key) { + return Ok(entry.clone()); } - let server = match server_type { - Server::Local => { - let server = Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())); - Ok::, FlowyError>(server) - }, - Server::AppFlowyCloud => { - let config = self.config.cloud_config.clone().ok_or_else(|| { - FlowyError::internal().with_context("AppFlowyCloud configuration is missing") - })?; - let server = Arc::new(AppFlowyCloudServer::new( - config, + let server: Arc = match key { + ServerType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + ServerType::AppFlowyCloud => { + let cfg = self + .config + .cloud_config + .clone() + .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?; + Arc::new(AppFlowyCloudServer::new( + cfg, self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), self.user.clone(), - )); - - Ok::, FlowyError>(server) + )) }, - }?; + }; - self.providers.insert(server_type.clone(), server.clone()); + self.providers.insert(key.clone(), server.clone()); Ok(server) } } -impl From for Server { - fn from(auth_provider: Authenticator) -> Self { - match auth_provider { - Authenticator::Local => Server::Local, - Authenticator::AppFlowyCloud => Server::AppFlowyCloud, - } - } -} - -impl From for Authenticator { - fn from(ty: Server) -> Self { - match ty { - Server::Local => Authenticator::Local, - Server::AppFlowyCloud => Authenticator::AppFlowyCloud, - } - } -} -impl From<&Authenticator> for Server { - fn from(auth_provider: &Authenticator) -> Self { - Self::from(auth_provider.clone()) - } -} - -pub fn current_server_type() -> Server { +/// Determine current server type from ENV +pub fn current_server_type() -> ServerType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => Server::Local, - AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, + AuthenticatorType::Local => ServerType::Local, + AuthenticatorType::AppFlowyCloud => ServerType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 2882b9b050..89ba14e6d2 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -14,11 +14,11 @@ use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; -use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; +use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; -use crate::server_layer::{Server, ServerProvider}; +use crate::server_layer::{ServerProvider, ServerType}; pub(crate) struct UserStatusCallbackImpl { pub(crate) collab_builder: Arc, @@ -49,16 +49,14 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn did_init( &self, user_id: i64, - user_authenticator: &Authenticator, + auth_type: &AuthType, cloud_config: &Option, user_workspace: &UserWorkspace, _device_id: &str, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; - self - .server_provider - .set_user_authenticator(user_authenticator); + self.server_provider.set_auth_type(*auth_type); if let Some(cloud_config) = cloud_config { self @@ -83,7 +81,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator == &Authenticator::Local) + .initialize(user_id, authenticator == &AuthType::Local) .await?; self.document_manager.initialize(user_id).await?; @@ -97,7 +95,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_id: i64, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { event!( tracing::Level::TRACE, @@ -127,11 +125,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_profile: &UserProfile, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { - self - .server_provider - .set_user_authenticator(&user_profile.authenticator); + self.server_provider.set_auth_type(*auth_type); let server_type = self.server_provider.get_server_type(); event!( @@ -159,16 +155,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await { Ok(doc_state) => match server_type { - Server::Local => FolderInitDataSource::LocalDisk { + ServerType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - Server::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + ServerType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), }, Err(err) => match server_type { - Server::Local => FolderInitDataSource::LocalDisk { + ServerType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - Server::AppFlowyCloud => { + ServerType::AppFlowyCloud => { return Err(err); }, }, @@ -188,7 +184,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .database_manager - .initialize_with_new_user(user_profile.uid, authenticator.is_local()) + .initialize_with_new_user(user_profile.uid, auth_type.is_local()) .await .context("DatabaseManager error")?; @@ -212,7 +208,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { &self, user_id: i64, user_workspace: &UserWorkspace, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { self .folder_manager diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index 0710bcc2b2..eb2bf26698 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -3,9 +3,8 @@ use client_api::entity::auth_dto::{UpdateUserParams, UserMetaData}; use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFWorkspaceMember}; use flowy_user_pub::entities::{ - Authenticator, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, - WorkspaceMember, USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, - USER_METADATA_STABILITY_AI_KEY, + AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, + USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, }; use crate::af_cloud::impls::user::util::encryption_type_from_profile; @@ -60,7 +59,7 @@ pub fn user_profile_from_af_profile( icon_url: icon_url.unwrap_or_default(), openai_key: openai_key.unwrap_or_default(), stability_ai_key: stability_ai_key.unwrap_or_default(), - authenticator: Authenticator::AppFlowyCloud, + authenticator: AuthType::AppFlowyCloud, encryption_type, uid: profile.uid, updated_at: profile.updated_at, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dde94aebdc..fd283360de 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -20,7 +20,7 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, Authenticator, Role, UpdateUserProfileParams, UserCredentials, UserProfile, + AuthResponse, AuthType, Role, UpdateUserProfileParams, UserCredentials, UserProfile, UserTokenState, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; @@ -84,13 +84,9 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - /// Sets the authenticator when user sign in or sign up. - /// - /// # Arguments - /// * `authenticator`: An `Authenticator` object. - fn set_user_authenticator(&self, authenticator: &Authenticator); + fn set_auth_type(&self, auth_type: &AuthType); - fn get_user_authenticator(&self) -> Authenticator; + fn get_auth_type(&self) -> AuthType; /// Sets the network reachability /// diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 857a735edb..c09b536ccf 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -32,7 +32,7 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, - pub auth_type: Authenticator, + pub auth_type: AuthType, } #[derive(Serialize, Deserialize, Default, Debug)] @@ -40,7 +40,7 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, - pub auth_type: Authenticator, + pub auth_type: AuthType, pub device_id: String, } @@ -103,7 +103,7 @@ impl UserAuthResponse for AuthResponse { #[derive(Clone, Debug)] pub struct UserCredentials { - /// Currently, the token is only used when the [Authenticator] is AppFlowyCloud + /// Currently, the token is only used when the [AuthType] is AppFlowyCloud pub token: Option, /// The user id @@ -180,7 +180,7 @@ pub struct UserProfile { pub icon_url: String, pub openai_key: String, pub stability_ai_key: String, - pub authenticator: Authenticator, + pub authenticator: AuthType, // If the encryption_sign is not empty, which means the user has enabled the encryption. pub encryption_type: EncryptionType, pub updated_at: i64, @@ -226,11 +226,11 @@ impl FromStr for EncryptionType { } } -impl From<(&T, &Authenticator)> for UserProfile +impl From<(&T, &AuthType)> for UserProfile where T: UserAuthResponse, { - fn from(params: (&T, &Authenticator)) -> Self { + fn from(params: (&T, &AuthType)) -> Self { let (value, auth_type) = params; let (icon_url, openai_key, stability_ai_key) = { value @@ -258,7 +258,7 @@ where token: value.user_token().unwrap_or_default(), icon_url, openai_key, - authenticator: auth_type.clone(), + authenticator: *auth_type, encryption_type: value.encryption_type(), stability_ai_key, updated_at: value.updated_at(), @@ -349,9 +349,9 @@ impl UpdateUserProfileParams { } } -#[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] -pub enum Authenticator { +pub enum AuthType { /// It's a local server, we do fake sign in default. Local = 0, /// Currently not supported. It will be supported in the future when the @@ -359,28 +359,28 @@ pub enum Authenticator { AppFlowyCloud = 1, } -impl Default for Authenticator { +impl Default for AuthType { fn default() -> Self { Self::Local } } -impl Authenticator { +impl AuthType { pub fn is_local(&self) -> bool { - matches!(self, Authenticator::Local) + matches!(self, AuthType::Local) } pub fn is_appflowy_cloud(&self) -> bool { - matches!(self, Authenticator::AppFlowyCloud) + matches!(self, AuthType::AppFlowyCloud) } } -impl From for Authenticator { +impl From for AuthType { fn from(value: i32) -> Self { match value { - 0 => Authenticator::Local, - 1 => Authenticator::AppFlowyCloud, - _ => Authenticator::Local, + 0 => AuthType::Local, + 1 => AuthType::AppFlowyCloud, + _ => AuthType::Local, } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 87e828199c..6a889359c1 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -235,20 +235,20 @@ pub enum AuthenticatorPB { AppFlowyCloud = 2, } -impl From for AuthenticatorPB { - fn from(auth_type: Authenticator) -> Self { +impl From for AuthenticatorPB { + fn from(auth_type: AuthType) -> Self { match auth_type { - Authenticator::Local => AuthenticatorPB::Local, - Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, + AuthType::Local => AuthenticatorPB::Local, + AuthType::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, } } } -impl From for Authenticator { +impl From for AuthType { fn from(pb: AuthenticatorPB) -> Self { match pb { - AuthenticatorPB::Local => Authenticator::Local, - AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, + AuthenticatorPB::Local => AuthType::Local, + AuthenticatorPB::AppFlowyCloud => AuthType::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index c0b7a8d6c2..a20229b6d0 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -45,16 +45,14 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_user_authenticator(); + let old_authenticator = manager.cloud_services.get_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), Err(err) => { - manager - .cloud_services - .set_user_authenticator(&old_authenticator); + manager.cloud_services.set_auth_type(&old_authenticator); return Err(err); }, } @@ -76,15 +74,13 @@ pub async fn sign_up( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: SignUpParams = data.into_inner().try_into()?; - let authenticator = params.auth_type.clone(); + let auth_type = params.auth_type; - let prev_authenticator = manager.cloud_services.get_user_authenticator(); - match manager.sign_up(authenticator, BoxAny::new(params)).await { + let prev_auth_type = manager.cloud_services.get_auth_type(); + match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager - .cloud_services - .set_user_authenticator(&prev_authenticator); + manager.cloud_services.set_auth_type(&prev_auth_type); Err(err) }, } @@ -119,7 +115,7 @@ pub async fn get_user_profile_handler( // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.authenticator == Authenticator::Local { + if user_profile.authenticator == AuthType::Local { user_profile.email = "".to_string(); } @@ -341,7 +337,7 @@ pub async fn oauth_sign_in_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: Authenticator = params.authenticator.into(); + let authenticator: AuthType = params.authenticator.into(); let user_profile = manager .sign_up(authenticator, BoxAny::new(params.map)) .await?; @@ -355,7 +351,7 @@ pub async fn gen_sign_in_url_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: Authenticator = params.authenticator.into(); + let authenticator: AuthType = params.authenticator.into(); let sign_in_url = manager .generate_sign_in_url_with_email(&authenticator, ¶ms.email) .await?; diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 7ed4948771..953011bc1c 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -86,12 +86,12 @@ pub fn init(user_manager: Weak) -> AFPlugin { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { - /// Only use when the [Authenticator] is Local or SelfHosted + /// Only use when the [AuthType] is Local or SelfHosted /// Logging into an account using a register email and password #[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")] SignInWithEmailPassword = 0, - /// Only use when the [Authenticator] is Local or SelfHosted + /// Only use when the [AuthType] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, @@ -129,7 +129,7 @@ pub enum UserEvent { OauthSignIn = 10, /// Get the OAuth callback url - /// Only use when the [Authenticator] is AFCloud + /// Only use when the [AuthType] is AFCloud #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")] GenerateSignInURL = 11, @@ -165,7 +165,7 @@ pub enum UserEvent { OpenAnonUser = 26, /// Push a realtime event to the user. Currently, the realtime event - /// is only used when the auth type is: [Authenticator::Supabase]. + /// is only used when the auth type is: [AuthType::Supabase]. /// #[event(input = "RealtimePayloadPB")] PushRealtimeEvent = 27, @@ -281,19 +281,19 @@ pub enum UserEvent { #[async_trait] pub trait UserStatusCallback: Send + Sync + 'static { - /// When the [Authenticator] changed, this method will be called. Currently, the auth type + /// When the [AuthType] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn authenticator_did_changed(&self, _authenticator: Authenticator) {} + fn authenticator_did_changed(&self, _authenticator: AuthType) {} /// This will be called after the application launches if the user is already signed in. /// If the user is not signed in, this method will not be called async fn did_init( &self, _user_id: i64, - _user_authenticator: &Authenticator, + _user_authenticator: &AuthType, _cloud_config: &Option, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -303,7 +303,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -314,7 +314,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_profile: &UserProfile, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -326,7 +326,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { &self, _user_id: i64, _user_workspace: &UserWorkspace, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 3056f4d945..5a8bb4d516 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -7,7 +7,7 @@ use tracing::{instrument, trace}; use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use flowy_user_pub::session::Session; @@ -39,7 +39,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { trace!( "migrate key with workspace id:{}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index e557c22450..ab828e18d8 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -11,7 +11,7 @@ use tracing::{event, instrument}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -41,12 +41,12 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { &self, session: &Session, collab_db: &Arc, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. // - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version. - if !matches!(authenticator, Authenticator::Local) { + if !matches!(authenticator, AuthType::Local) { return Ok(()); } collab_db.with_write_txn(|write_txn| { diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index c604e47e8d..1330d1995b 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -7,7 +7,7 @@ use flowy_error::FlowyResult; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_data_migration_records; use flowy_sqlite::ConnectionPool; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use flowy_user_pub::session::Session; use semver::Version; use tracing::info; @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - authenticator: &Authenticator, + authenticator: &AuthType, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -98,7 +98,7 @@ pub trait UserDataMigration { &self, user: &Session, collab_db: &Arc, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index ec55b5fe29..51894f9a04 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -7,7 +7,7 @@ use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -39,7 +39,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index d631e32e78..70123edb2c 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -7,7 +7,7 @@ use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -37,7 +37,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 9efbf81932..106e911c1e 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -30,7 +30,7 @@ use flowy_folder_pub::entities::{ }; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; -use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; +use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; use flowy_user_pub::session::Session; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; @@ -1175,7 +1175,7 @@ pub async fn upload_collab_objects_data( uid: i64, user_collab_db: Weak, workspace_id: &Uuid, - user_authenticator: &Authenticator, + user_authenticator: &AuthType, collab_data: ImportedCollabData, user_cloud_service: Arc, ) -> Result<(), FlowyError> { diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index e2b6628832..1138efd092 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -31,8 +31,8 @@ pub struct UserTable { } #[allow(deprecated)] -impl From<(UserProfile, Authenticator)> for UserTable { - fn from(value: (UserProfile, Authenticator)) -> Self { +impl From<(UserProfile, AuthType)> for UserTable { + fn from(value: (UserProfile, AuthType)) -> Self { let (user_profile, auth_type) = value; let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTable { @@ -62,7 +62,7 @@ impl From for UserProfile { token: table.token, icon_url: table.icon_url, openai_key: table.openai_key, - authenticator: Authenticator::from(table.auth_type), + authenticator: AuthType::from(table.auth_type), encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), stability_ai_key: table.stability_ai_key, updated_at: table.updated_at, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 5b5d060539..89a05539b6 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -141,7 +141,7 @@ impl UserManager { // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.authenticator != Authenticator::Local && user.authenticator != current_authenticator { + if user.authenticator != AuthType::Local && user.authenticator != current_authenticator { event!( tracing::Level::INFO, "Authenticator changed from {:?} to {:?}", @@ -349,9 +349,9 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - authenticator: Authenticator, + authenticator: AuthType, ) -> Result { - self.cloud_services.set_user_authenticator(&authenticator); + self.cloud_services.set_auth_type(&authenticator); let response: AuthResponse = self .cloud_services @@ -398,25 +398,25 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_up( &self, - authenticator: Authenticator, + auth_type: AuthType, params: BoxAny, ) -> Result { // sign out the current user if there is one - let migration_user = self.get_migration_user(&authenticator).await; - self.cloud_services.set_user_authenticator(&authenticator); + let migration_user = self.get_migration_user(&auth_type).await; + self.cloud_services.set_auth_type(&auth_type); let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; - let new_user_profile = UserProfile::from((&response, &authenticator)); + let new_user_profile = UserProfile::from((&response, &auth_type)); if new_user_profile.encryption_type.require_encrypt_secret() { self.auth_process.lock().await.replace(UserAuthProcess { user_profile: new_user_profile.clone(), migration_user, response, - authenticator, + authenticator: auth_type, }); } else { self - .continue_sign_up(&new_user_profile, migration_user, response, &authenticator) + .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) .await?; } Ok(new_user_profile) @@ -450,12 +450,12 @@ impl UserManager { new_user_profile: &UserProfile, migration_user: Option, response: AuthResponse, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, authenticator, &new_session) + .save_auth_data(&response, auth_type, &new_session) .await?; let _ = self .initial_user_awareness(&new_session, &new_user_profile.authenticator) @@ -469,7 +469,7 @@ impl UserManager { new_user_profile, &new_session.user_workspace, &self.authenticate_user.user_config.device_id, - authenticator, + auth_type, ) .await?; @@ -493,7 +493,7 @@ impl UserManager { new_user_profile.uid ); self - .migrate_anon_user_data_to_cloud(&old_user, &new_session, authenticator) + .migrate_anon_user_data_to_cloud(&old_user, &new_session, auth_type) .await?; self.remove_anon_user(); let _ = self @@ -709,10 +709,10 @@ impl UserManager { pub(crate) async fn generate_sign_in_url_with_email( &self, - authenticator: &Authenticator, + authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_user_authenticator(authenticator); + self.cloud_services.set_auth_type(authenticator); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; @@ -724,9 +724,7 @@ impl UserManager { email: &str, password: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) @@ -737,9 +735,7 @@ impl UserManager { email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) @@ -752,9 +748,7 @@ impl UserManager { email: &str, passcode: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) @@ -764,9 +758,7 @@ impl UserManager { &self, oauth_provider: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) @@ -778,7 +770,7 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - authenticator: &Authenticator, + authenticator: &AuthType, session: &Session, ) -> Result<(), FlowyError> { let user_profile = UserProfile::from((response, authenticator)); @@ -798,7 +790,7 @@ impl UserManager { .authenticate_user .set_session(Some(session.clone().into()))?; self - .save_user(uid, (user_profile, authenticator.clone()).into()) + .save_user(uid, (user_profile, *authenticator).into()) .await?; Ok(()) } @@ -827,14 +819,14 @@ impl UserManager { &self, old_user: &AnonUser, _new_user_session: &Session, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - if authenticator == &Authenticator::AppFlowyCloud { + if authenticator == &AuthType::AppFlowyCloud { self .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) .await?; @@ -853,10 +845,10 @@ impl UserManager { } } -fn current_authenticator() -> Authenticator { +fn current_authenticator() -> AuthType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => Authenticator::Local, - AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud, + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index b7f4789f9a..ddb73667c8 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -4,7 +4,7 @@ use tracing::instrument; use crate::entities::UserProfilePB; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; @@ -12,10 +12,7 @@ use flowy_user_pub::session::Session; pub const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] - pub async fn get_migration_user( - &self, - current_authenticator: &Authenticator, - ) -> Option { + pub async fn get_migration_user(&self, current_authenticator: &AuthType) -> Option { // No need to migrate if the user is already local if current_authenticator.is_local() { return None; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index d055621398..7de09995a0 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -12,7 +12,7 @@ use collab_integrate::CollabKVDB; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; use dashmap::try_result::TryResult; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; +use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; use tracing::{error, info, instrument, trace}; use uuid::Uuid; @@ -119,9 +119,8 @@ impl UserManager { pub(crate) async fn initial_user_awareness( &self, session: &Session, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { - let authenticator = authenticator.clone(); let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); // Try to acquire mutable access to `is_loading_awareness`. @@ -156,11 +155,11 @@ impl UserManager { let is_exist_on_disk = self .authenticate_user .is_collab_on_disk(session.user_id, &object_id.to_string())?; - if authenticator.is_local() || is_exist_on_disk { + if auth_type.is_local() || is_exist_on_disk { trace!( "Initializing new user awareness from disk:{}, {:?}", object_id, - authenticator + auth_type ); let collab_db = self.get_collab_db(session.user_id)?; let workspace_id = session.user_workspace.workspace_id()?; @@ -185,9 +184,9 @@ impl UserManager { } else { info!( "Initializing new user awareness from server:{}, {:?}", - object_id, authenticator + object_id, auth_type ); - self.load_awareness_from_server(session, object_id, authenticator.clone())?; + self.load_awareness_from_server(session, object_id, *auth_type)?; } } else { return Err(FlowyError::new( @@ -209,7 +208,7 @@ impl UserManager { &self, session: &Session, object_id: Uuid, - authenticator: Authenticator, + authenticator: AuthType, ) -> FlowyResult<()> { // Clone necessary data let session = session.clone(); diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs index 906002ad10..ed7fdacf8d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs @@ -1,11 +1,11 @@ use crate::migrations::AnonUser; -use flowy_user_pub::entities::{AuthResponse, Authenticator, UserProfile}; +use flowy_user_pub::entities::{AuthResponse, AuthType, UserProfile}; /// recording the intermediate state of the sign-in/sign-up process #[derive(Clone)] pub struct UserAuthProcess { pub user_profile: UserProfile, pub response: AuthResponse, - pub authenticator: Authenticator, + pub authenticator: AuthType, pub migration_user: Option, } From d8401e09c9c48468cb9c61b0c77809dc74c19a0c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:10:17 +0800 Subject: [PATCH 355/384] feat: implement keyboard triggering on buttons and add focus state (#7724) * feat: implement keyboard triggering on buttons and add focus state * chore: pass isFocused to builders --- .../button/base_button/base_button.dart | 116 +++++++++++++++--- .../button/filled_button/filled_button.dart | 2 +- .../filled_button/filled_text_button.dart | 2 +- .../button/ghost_button/ghost_button.dart | 2 +- .../ghost_button/ghost_icon_text_button.dart | 2 +- .../ghost_button/ghost_text_button.dart | 2 +- .../outlined_button/outlined_button.dart | 8 +- .../outlined_icon_text_button.dart | 8 +- .../outlined_button/outlined_text_button.dart | 8 +- 9 files changed, 113 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart index 22c5325681..b62f2cce9b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -7,6 +7,13 @@ typedef AFBaseButtonColorBuilder = Color Function( bool disabled, ); +typedef AFBaseButtonBorderColorBuilder = Color Function( + BuildContext context, + bool isHovering, + bool disabled, + bool isFocused, +); + class AFBaseButton extends StatefulWidget { const AFBaseButton({ super.key, @@ -16,49 +23,99 @@ class AFBaseButton extends StatefulWidget { required this.borderRadius, this.borderColor, this.backgroundColor, + this.ringColor, this.disabled = false, }); final VoidCallback? onTap; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? ringColor; final AFBaseButtonColorBuilder? backgroundColor; final EdgeInsetsGeometry padding; final double borderRadius; final bool disabled; - final Widget Function(BuildContext context, bool isHovering, bool disabled) - builder; + final Widget Function( + BuildContext context, + bool isHovering, + bool disabled, + ) builder; @override State createState() => _AFBaseButtonState(); } class _AFBaseButtonState extends State { + final FocusNode focusNode = FocusNode(); + bool isHovering = false; + bool isFocused = false; + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final Color borderColor = _buildBorderColor(context); final Color backgroundColor = _buildBackgroundColor(context); + final Color ringColor = _buildRingColor(context); - return MouseRegion( - cursor: - widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - onTap: widget.disabled ? null : widget.onTap, - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Padding( - padding: widget.padding, - child: widget.builder(context, isHovering, widget.disabled), + return Actions( + actions: { + ActivateIntent: CallbackAction( + onInvoke: (_) { + if (!widget.disabled) { + widget.onTap?.call(); + } + return; + }, + ), + }, + child: Focus( + focusNode: focusNode, + onFocusChange: (isFocused) { + setState(() => this.isFocused = isFocused); + }, + child: MouseRegion( + cursor: widget.disabled + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: GestureDetector( + onTap: widget.disabled ? null : widget.onTap, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + border: isFocused + ? Border.all( + color: ringColor, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Padding( + padding: widget.padding, + child: widget.builder( + context, + isHovering, + widget.disabled, + ), + ), + ), + ), ), ), ), @@ -67,7 +124,8 @@ class _AFBaseButtonState extends State { Color _buildBorderColor(BuildContext context) { final theme = AppFlowyTheme.of(context); - return widget.borderColor?.call(context, isHovering, widget.disabled) ?? + return widget.borderColor + ?.call(context, isHovering, widget.disabled, isFocused) ?? theme.borderColorScheme.greyTertiary; } @@ -76,4 +134,22 @@ class _AFBaseButtonState extends State { return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? theme.fillColorScheme.transparent; } + + Color _buildRingColor(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + if (widget.ringColor != null) { + return widget.ringColor! + .call(context, isHovering, widget.disabled, isFocused); + } + + if (isFocused) { + return AppFlowyTheme.of(context) + .borderColorScheme + .themeThick + .withAlpha(128); + } + + return theme.borderColorScheme.transparent; + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart index 68fb341827..e871626b59 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart @@ -115,7 +115,7 @@ class AFFilledButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart index 889ad1e429..d1b1d868d0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -121,7 +121,7 @@ class AFFilledTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart index 47ff96e878..6300c6f5a8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart @@ -86,7 +86,7 @@ class AFGhostButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart index e65eb2dd7e..af65599ea3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart @@ -109,7 +109,7 @@ class AFGhostIconTextButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { return Colors.transparent; }, padding: padding ?? size.buildPadding(context), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart index 441b544f8a..d154d67dbd 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart @@ -88,7 +88,7 @@ class AFGhostTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart index 3b0ea7a06d..205d9931d6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart @@ -38,7 +38,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -79,7 +79,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -118,7 +118,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: true, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -148,7 +148,7 @@ class AFOutlinedButton extends StatelessWidget { final EdgeInsetsGeometry? padding; final double? borderRadius; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; final AFOutlinedButtonWidgetBuilder builder; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart index 710a4ccca5..350594cd46 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart @@ -46,7 +46,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -101,7 +101,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -156,7 +156,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -190,7 +190,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { final AFOutlinedIconBuilder iconBuilder; final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; @override diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart index d5ae580583..d809d981b0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -40,7 +40,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -95,7 +95,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -150,7 +150,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -173,7 +173,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ); } - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; @override Widget build(BuildContext context) { From 068f93c258eaad91d9a4025598d673fd1e1cacf5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:14:28 +0800 Subject: [PATCH 356/384] feat: add shadow tokens (#7726) --- .../document/presentation/editor_page.dart | 2 +- .../link/link_create_menu.dart | 2 +- .../link_preview/paste_as/paste_as_menu.dart | 2 +- .../lib/src/theme/data/builder.dart | 68 +++++++++++-------- .../appflowy_ui/lib/src/theme/data/data.dart | 22 +++--- .../lib/src/theme/shadow/shadow.dart | 9 ++- 6 files changed, 60 insertions(+), 45 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 0ffb7de73a..edb19232be 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -445,7 +445,7 @@ class _AppFlowyEditorPageState extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(appTheme.borderRadius.l), color: appTheme.surfaceColorScheme.primary, - boxShadow: [appTheme.shadow.small], + boxShadow: appTheme.shadow.small, ), toolbarBuilder: (_, child, onDismiss, isMetricsChanged) => BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index 6213896feb..002d569c7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -315,6 +315,6 @@ ShapeDecoration buildToolbarLinkDecoration( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), - shadows: [theme.shadow.small], + shadows: theme.shadow.small, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart index d31b2f8fd9..fb51cdcf47 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart @@ -148,7 +148,7 @@ class _PasteAsMenuState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: theme.surfaceColorScheme.primary, - boxShadow: [theme.shadow.medium], + boxShadow: theme.shadow.medium, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 8923f61e1f..06c6eb5d8e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -93,35 +93,6 @@ class AppFlowyThemeBuilder { }; } - AppFlowyShadow buildShadow(Brightness brightness) { - return switch (brightness) { - Brightness.light => AppFlowyShadow( - small: const BoxShadow( - offset: Offset(0.0, 2.0), - blurRadius: 16.0, - color: Color(0x1F000000), - ), - medium: const BoxShadow( - offset: Offset(0.0, 4.0), - blurRadius: 32.0, - color: Color(0x1F000000), - ), - ), - Brightness.dark => AppFlowyShadow( - small: BoxShadow( - offset: Offset(0.0, 2.0), - blurRadius: 16.0, - color: Color(0x7A000000), - ), - medium: BoxShadow( - offset: Offset(0.0, 4.0), - blurRadius: 32.0, - color: Color(0x7A000000), - ), - ), - }; - } - AppFlowyBorderColorScheme buildBorderColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, @@ -351,4 +322,43 @@ class AppFlowyThemeBuilder { xxl: AppFlowySpacingConstant.spacing600, ); } + + AppFlowyShadow buildShadow( + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyShadow( + small: [ + BoxShadow( + offset: Offset(0, 2), + blurRadius: 16, + color: Color(0x1F000000), + ), + ], + medium: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 32, + color: Color(0x1F000000), + ), + ], + ), + Brightness.dark => AppFlowyShadow( + small: [ + BoxShadow( + offset: Offset(0, 2), + blurRadius: 16, + color: Color(0x7A000000), + ), + ], + medium: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 32, + color: Color(0x7A000000), + ), + ], + ), + }; + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart index 9494bdf0e2..60f7d1f1a4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart @@ -36,9 +36,9 @@ abstract class AppFlowyBaseTheme { AppFlowySpacing get spacing; - AppFlowyBrandColorScheme get brandColorScheme; - AppFlowyShadow get shadow; + + AppFlowyBrandColorScheme get brandColorScheme; } class AppFlowyThemeData extends AppFlowyBaseTheme { @@ -70,10 +70,11 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.light, ); - final shadow = themeBuilder.buildShadow(Brightness.light); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); + final shadow = themeBuilder.buildShadow(Brightness.light); + return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -85,8 +86,8 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { surfaceColorScheme: surfaceColorScheme, borderRadius: borderRadius, spacing: spacing, - brandColorScheme: brandColorScheme, shadow: shadow, + brandColorScheme: brandColorScheme, ); } @@ -117,10 +118,11 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.dark, ); - final shadow = themeBuilder.buildShadow(Brightness.dark); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); + final shadow = themeBuilder.buildShadow(Brightness.dark); + return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -132,8 +134,8 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { surfaceColorScheme: surfaceColorScheme, borderRadius: borderRadius, spacing: spacing, - brandColorScheme: brandColorScheme, shadow: shadow, + brandColorScheme: brandColorScheme, ); } @@ -146,10 +148,10 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { required this.surfaceColorScheme, required this.borderRadius, required this.spacing, + required this.shadow, required this.brandColorScheme, required this.iconColorTheme, required this.backgroundColorScheme, - required this.shadow, this.brightness = Brightness.light, }); @@ -181,6 +183,9 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { @override final AppFlowySpacing spacing; + @override + final AppFlowyShadow shadow; + @override final AppFlowyBrandColorScheme brandColorScheme; @@ -190,9 +195,6 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { @override final AppFlowyBackgroundColorScheme backgroundColorScheme; - @override - final AppFlowyShadow shadow; - static AppFlowyTextColorScheme buildTextColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart index 9bb2ac1116..457b86265e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart @@ -1,8 +1,11 @@ import 'package:flutter/widgets.dart'; class AppFlowyShadow { - AppFlowyShadow({required this.small, required this.medium}); + AppFlowyShadow({ + required this.small, + required this.medium, + }); - final BoxShadow small; - final BoxShadow medium; + final List small; + final List medium; } From e1bfb7095b7d296350ef3a9d0bcb6232960a4f43 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:20:25 +0800 Subject: [PATCH 357/384] feat: improve select modal button (#7736) --- .../prompt_input/select_model_menu.dart | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index eef2663370..a611d84310 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -31,9 +31,6 @@ class _SelectModelMenuState extends State { ), child: BlocBuilder( builder: (context, state) { - if (state.selectedModel == null) { - return const SizedBox.shrink(); - } return AppFlowyPopover( offset: Offset(-12.0, 0.0), constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), @@ -55,8 +52,12 @@ class _SelectModelMenuState extends State { ); }, child: _CurrentModelButton( - model: state.selectedModel!, - onTap: () => popoverController.show(), + model: state.selectedModel, + onTap: () { + if (state.selectedModel != null) { + popoverController.show(); + } + }, ), ); }, @@ -202,7 +203,7 @@ class _CurrentModelButton extends StatelessWidget { required this.onTap, }); - final AIModelPB model; + final AIModelPB? model; final VoidCallback onTap; @override @@ -214,40 +215,45 @@ class _CurrentModelButton extends StatelessWidget { behavior: HitTestBehavior.opaque, child: SizedBox( height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(4.0), - child: Row( - children: [ - Padding( - // TODO: remove this after change icon to 20px - padding: EdgeInsets.all(2), - child: FlowySvg( - FlowySvgs.ai_sparks_s, - color: Theme.of(context).hintColor, - size: Size.square(16), - ), - ), - if (!model.isDefault) + child: AnimatedSize( + duration: const Duration(milliseconds: 50), + curve: Curves.easeInOut, + alignment: AlignmentDirectional.centerStart, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(4.0), + child: Row( + children: [ Padding( - padding: EdgeInsetsDirectional.only(end: 2.0), - child: FlowyText( - model.i18n, - fontSize: 12, - figmaLineHeight: 16, + // TODO: remove this after change icon to 20px + padding: EdgeInsets.all(2), + child: FlowySvg( + FlowySvgs.ai_sparks_s, color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, + size: Size.square(16), ), ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], + if (model != null && !model!.isDefault) + Padding( + padding: EdgeInsetsDirectional.only(end: 2.0), + child: FlowyText( + model!.i18n, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), ), ), ), From b12bd8ee8562bfffcea0778f296ff2f5eac7c7fd Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:03:49 +0800 Subject: [PATCH 358/384] feat: add medium sized text field (#7737) * feat: add medium sized text field * chore: remove height --- .../continue_with_email_and_password.dart | 1 - ...inue_with_magic_link_or_passcode_page.dart | 1 - .../appflowy_ui/example/lib/main.dart | 6 +-- .../lib/src/textfield/textfield_page.dart | 13 +++++ .../src/component/textfield/textfield.dart | 50 ++++++++++++------- 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index 349dd7e0d4..5027874418 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -60,7 +60,6 @@ class _ContinueWithEmailAndPasswordState key: emailKey, controller: controller, hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), - radius: 10, onSubmitted: (value) => _signInWithEmail( context, value, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index 8cfc4c1157..ec4fd1bbee 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -101,7 +101,6 @@ class _ContinueWithMagicLinkOrPasscodePageState controller: passcodeController, hintText: LocaleKeys.signIn_enterCode.tr(), keyboardType: TextInputType.number, - radius: 10, autoFocus: true, onSubmitted: (passcode) { if (passcode.isEmpty) { diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 067e42858b..41548abd08 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -24,6 +24,8 @@ class MyApp extends StatelessWidget { return ValueListenableBuilder( valueListenable: themeMode, builder: (context, themeMode, child) { + final themeData = + themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); return AppFlowyTheme( data: themeMode == ThemeMode.light ? AppFlowyThemeData.light() @@ -31,9 +33,7 @@ class MyApp extends StatelessWidget { child: MaterialApp( debugShowCheckedModeBanner: false, title: 'AppFlowy UI Example', - theme: themeMode == ThemeMode.light - ? ThemeData.light() - : ThemeData.dark(), + theme: themeData.copyWith(visualDensity: VisualDensity.standard), home: const MyHomePage( title: 'AppFlowy UI', ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart index 280e43818c..9e3436ecd4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart @@ -11,6 +11,19 @@ class TextFieldPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildSection( + 'TextField Sizes', + [ + AFTextField( + hintText: 'Please enter your name', + size: AFTextFieldSize.m, + ), + AFTextField( + hintText: 'Please enter your name', + ), + ], + ), + const SizedBox(height: 32), _buildSection( 'TextField with hint text', [ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index a934734983..3f5ad4cfed 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:appflowy_ui/src/theme/theme.dart'; import 'package:flutter/material.dart'; typedef AFTextFieldValidator = (bool result, String errorText) Function( @@ -20,21 +20,17 @@ class AFTextField extends StatefulWidget { this.hintText, this.initialText, this.keyboardType, - this.radius, + this.size = AFTextFieldSize.l, this.validator, this.controller, this.onChanged, this.onSubmitted, this.autoFocus, - this.height = 40.0, this.obscureText = false, this.suffixIconBuilder, this.suffixIconConstraints, }); - /// The height of the text field. - final double height; - /// The hint text to display when the text field is empty. final String? hintText; @@ -44,8 +40,8 @@ class AFTextField extends StatefulWidget { /// The type of keyboard to display. final TextInputType? keyboardType; - /// The radius of the text field. - final double? radius; + /// The size variant of the text field. + final AFTextFieldSize size; /// The validator to use for the text field. final AFTextFieldValidator? validator; @@ -115,9 +111,8 @@ class _AFTextFieldState extends AFTextFieldState { @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - final borderRadius = BorderRadius.circular( - widget.radius ?? theme.borderRadius.l, - ); + final borderRadius = widget.size.borderRadius(theme); + final contentPadding = widget.size.contentPadding(theme); final errorBorderColor = theme.borderColorScheme.errorThick; final defaultBorderColor = theme.borderColorScheme.greyTertiary; @@ -137,10 +132,9 @@ class _AFTextFieldState extends AFTextFieldState { hintStyle: theme.textStyle.body.standard( color: theme.textColorScheme.tertiary, ), - contentPadding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: 10, - ), + isDense: true, + constraints: BoxConstraints(), + contentPadding: contentPadding, border: OutlineInputBorder( borderSide: BorderSide( color: hasError ? errorBorderColor : defaultBorderColor, @@ -179,8 +173,6 @@ class _AFTextFieldState extends AFTextFieldState { ), ); - child = SizedBox(height: widget.height, child: child); - if (hasError && errorText.isNotEmpty) { child = Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -236,3 +228,27 @@ class _AFTextFieldState extends AFTextFieldState { }); } } + +enum AFTextFieldSize { + m, + l; + + EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { + return EdgeInsets.symmetric( + vertical: switch (this) { + AFTextFieldSize.m => theme.spacing.s, + AFTextFieldSize.l => 10.0, + }, + horizontal: theme.spacing.m, + ); + } + + BorderRadius borderRadius(AppFlowyThemeData theme) { + return BorderRadius.circular( + switch (this) { + AFTextFieldSize.m => theme.borderRadius.m, + AFTextFieldSize.l => 10.0, + }, + ); + } +} From 0906febe957ffd982fb3283087c11651457c168a Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:48:17 +0800 Subject: [PATCH 359/384] refactor: remove server type --- .../src/deps_resolve/cloud_service_impl.rs | 21 ++--- frontend/rust-lib/flowy-core/src/lib.rs | 19 ++--- .../rust-lib/flowy-core/src/server_layer.rs | 78 ++++--------------- .../flowy-core/src/user_state_callback.rs | 15 ++-- .../rust-lib/flowy-user-pub/src/entities.rs | 10 +++ 5 files changed, 49 insertions(+), 94 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 9c1ee9462c..56bf4e965c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,4 +1,4 @@ -use crate::server_layer::{ServerProvider, ServerType}; +use crate::server_layer::ServerProvider; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::workspace_dto::PublishInfoView; @@ -188,8 +188,8 @@ impl UserCloudServiceProvider for ServerProvider { /// When user login, the provider type is set by the [AuthType] and save to disk for next use. /// - /// Each [AuthType] has a corresponding [ServerType]. The [ServerType] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, + /// Each [AuthType] has a corresponding [AuthType]. The [AuthType] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, /// it will be used when user open the app again. /// fn set_auth_type(&self, auth_type: &AuthType) { @@ -211,7 +211,7 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.set_secret(secret); } - /// Returns the [UserCloudService] base on the current [ServerType]. + /// Returns the [UserCloudService] base on the current [AuthType]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let user_service = self.get_server()?.user_service(); @@ -219,9 +219,9 @@ impl UserCloudServiceProvider for ServerProvider { } fn service_url(&self) -> String { - match self.get_server_type() { - ServerType::Local => "".to_string(), - ServerType::AppFlowyCloud => AFCloudConfiguration::from_env() + match self.get_auth_type() { + AuthType::Local => "".to_string(), + AuthType::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), } @@ -578,12 +578,15 @@ impl DocumentCloudService for ServerProvider { impl CollabCloudPluginProvider for ServerProvider { fn provider_type(&self) -> CollabPluginProviderType { - self.get_server_type().into() + match self.get_auth_type() { + AuthType::Local => CollabPluginProviderType::Local, + AuthType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + } } fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { // If the user is local, we don't need to create a sync plugin. - if self.get_server_type().is_local() { + if self.get_auth_type().is_local() { debug!( "User authenticator is local, skip create sync plugin for: {}", context diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index bd55c8db1d..d217f52785 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,6 +1,6 @@ #![allow(unused_doc_comments)] -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, ServerProvider, ServerType}; +use crate::server_layer::{current_server_type, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -131,12 +131,12 @@ impl AppFlowyCore { store_preference.clone(), )); - let server_type = current_server_type(); - debug!("🔥runtime:{}, server:{}", runtime, server_type); + let auth_type = current_server_type(); + debug!("🔥runtime:{}, server:{}", runtime, auth_type); let server_provider = Arc::new(ServerProvider::new( config.clone(), - server_type, + auth_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -314,15 +314,6 @@ impl AppFlowyCore { } } -impl From for CollabPluginProviderType { - fn from(server_type: ServerType) -> Self { - match server_type { - ServerType::Local => CollabPluginProviderType::Local, - ServerType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, - } - } -} - struct ServerUserImpl(Weak); impl ServerUserImpl { diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index c0bf043d27..ebb86c4417 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -13,52 +13,12 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::fmt::{Display, Formatter, Result as FmtResult}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; -/// ServerType: local or cloud -#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum ServerType { - Local = 0, - AppFlowyCloud = 1, -} - -impl ServerType { - pub fn is_local(&self) -> bool { - matches!(self, Self::Local) - } -} - -impl Display for ServerType { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{:?}", self) - } -} - -/// Conversion between AuthType and ServerType -impl From<&AuthType> for ServerType { - fn from(a: &AuthType) -> Self { - match a { - AuthType::Local => ServerType::Local, - AuthType::AppFlowyCloud => ServerType::AppFlowyCloud, - } - } -} -impl From for AuthType { - fn from(s: ServerType) -> Self { - match s { - ServerType::Local => AuthType::Local, - ServerType::AppFlowyCloud => AuthType::AppFlowyCloud, - } - } -} - pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: DashMap>, + providers: DashMap>, auth_type: ArcSwap, user: Arc, pub local_ai: Arc, @@ -70,12 +30,11 @@ pub struct ServerProvider { impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - initial: ServerType, + initial_auth: AuthType, store_preferences: Weak, user_service: impl LoginUserService + 'static, ) -> Self { let user = Arc::new(user_service); - let initial_auth = AuthType::from(initial); let auth_type = ArcSwap::from(Arc::new(initial_auth)); let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; let ai_user = Arc::new(AIUserServiceImpl(user.clone())); @@ -98,17 +57,10 @@ impl ServerProvider { } } - /// Reads current type - pub fn get_server_type(&self) -> ServerType { - let auth_type = self.auth_type.load_full(); - ServerType::from(auth_type.as_ref()) - } - - pub fn set_auth_type(&self, a: AuthType) { - let old_type = self.get_server_type(); - self.auth_type.store(Arc::new(a)); - let new_type = self.get_server_type(); - if old_type != new_type { + pub fn set_auth_type(&self, new_auth_type: AuthType) { + let old_type = self.get_auth_type(); + if old_type != new_auth_type { + self.auth_type.store(Arc::new(new_auth_type)); self.providers.remove(&old_type); } } @@ -119,14 +71,14 @@ impl ServerProvider { /// Lazily create or fetch an AppFlowyServer instance pub fn get_server(&self) -> FlowyResult> { - let key = self.get_server_type(); - if let Some(entry) = self.providers.get(&key) { + let auth_type = self.get_auth_type(); + if let Some(entry) = self.providers.get(&auth_type) { return Ok(entry.clone()); } - let server: Arc = match key { - ServerType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), - ServerType::AppFlowyCloud => { + let server: Arc = match auth_type { + AuthType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + AuthType::AppFlowyCloud => { let cfg = self .config .cloud_config @@ -142,15 +94,15 @@ impl ServerProvider { }, }; - self.providers.insert(key.clone(), server.clone()); + self.providers.insert(auth_type, server.clone()); Ok(server) } } /// Determine current server type from ENV -pub fn current_server_type() -> ServerType { +pub fn current_server_type() -> AuthType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => ServerType::Local, - AuthenticatorType::AppFlowyCloud => ServerType::AppFlowyCloud, + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 89ba14e6d2..ff1c6b6699 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -18,7 +18,7 @@ use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; -use crate::server_layer::{ServerProvider, ServerType}; +use crate::server_layer::ServerProvider; pub(crate) struct UserStatusCallbackImpl { pub(crate) collab_builder: Arc, @@ -128,7 +128,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { auth_type: &AuthType, ) -> FlowyResult<()> { self.server_provider.set_auth_type(*auth_type); - let server_type = self.server_provider.get_server_type(); event!( tracing::Level::TRACE, @@ -154,17 +153,17 @@ impl UserStatusCallback for UserStatusCallbackImpl { ) .await { - Ok(doc_state) => match server_type { - ServerType::Local => FolderInitDataSource::LocalDisk { + Ok(doc_state) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - ServerType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), }, - Err(err) => match server_type { - ServerType::Local => FolderInitDataSource::LocalDisk { + Err(err) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - ServerType::AppFlowyCloud => { + AuthType::AppFlowyCloud => { return Err(err); }, }, diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index c09b536ccf..d59f9cab47 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use std::str::FromStr; use chrono::{DateTime, Utc}; @@ -359,6 +360,15 @@ pub enum AuthType { AppFlowyCloud = 1, } +impl Display for AuthType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AuthType::Local => write!(f, "Local"), + AuthType::AppFlowyCloud => write!(f, "AppFlowyCloud"), + } + } +} + impl Default for AuthType { fn default() -> Self { Self::Local From 2dc22004a1e249994f4569c058b77130b6723767 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:57:33 +0800 Subject: [PATCH 360/384] refactor: remove server type --- .../src/deps_resolve/cloud_service_impl.rs | 4 ++-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 4 ++-- .../rust-lib/flowy-user/src/event_handler.rs | 10 +++++---- .../flowy-user/src/user_manager/manager.rs | 22 +++++++++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 56bf4e965c..35300563d7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -192,11 +192,11 @@ impl UserCloudServiceProvider for ServerProvider { /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, /// it will be used when user open the app again. /// - fn set_auth_type(&self, auth_type: &AuthType) { + fn set_server_auth_type(&self, auth_type: &AuthType) { self.set_auth_type(*auth_type); } - fn get_auth_type(&self) -> AuthType { + fn get_server_auth_type(&self) -> AuthType { self.get_auth_type() } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index fd283360de..dd69e4aa37 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -84,9 +84,9 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - fn set_auth_type(&self, auth_type: &AuthType); + fn set_server_auth_type(&self, auth_type: &AuthType); - fn get_auth_type(&self) -> AuthType; + fn get_server_auth_type(&self) -> AuthType; /// Sets the network reachability /// diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index a20229b6d0..7a64c20e06 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -45,14 +45,16 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_auth_type(); + let old_authenticator = manager.cloud_services.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), Err(err) => { - manager.cloud_services.set_auth_type(&old_authenticator); + manager + .cloud_services + .set_server_auth_type(&old_authenticator); return Err(err); }, } @@ -76,11 +78,11 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_services.get_auth_type(); + let prev_auth_type = manager.cloud_services.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager.cloud_services.set_auth_type(&prev_auth_type); + manager.cloud_services.set_server_auth_type(&prev_auth_type); Err(err) }, } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 89a05539b6..8fb4009991 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -351,7 +351,7 @@ impl UserManager { params: SignInParams, authenticator: AuthType, ) -> Result { - self.cloud_services.set_auth_type(&authenticator); + self.cloud_services.set_server_auth_type(&authenticator); let response: AuthResponse = self .cloud_services @@ -403,7 +403,7 @@ impl UserManager { ) -> Result { // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_services.set_auth_type(&auth_type); + self.cloud_services.set_server_auth_type(&auth_type); let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); @@ -712,7 +712,7 @@ impl UserManager { authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_auth_type(authenticator); + self.cloud_services.set_server_auth_type(authenticator); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; @@ -724,7 +724,9 @@ impl UserManager { email: &str, password: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) @@ -735,7 +737,9 @@ impl UserManager { email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) @@ -748,7 +752,9 @@ impl UserManager { email: &str, passcode: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) @@ -758,7 +764,9 @@ impl UserManager { &self, oauth_provider: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) From 54b5e248e3aa7eccfdcff79b1de2abdea1a83b39 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:02:08 +0800 Subject: [PATCH 361/384] feat: implement modal (#7750) --- .../presentation/widgets/dialog_v2.dart | 109 +++++++++++++ .../appflowy_ui/example/lib/main.dart | 8 +- .../example/lib/src/modal/modal_page.dart | 153 ++++++++++++++++++ .../button/base_button/base_button.dart | 5 +- .../lib/src/component/component.dart | 1 + .../lib/src/component/modal/dimension.dart | 9 ++ .../lib/src/component/modal/modal.dart | 125 ++++++++++++++ 7 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart new file mode 100644 index 0000000000..43ab8897e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart @@ -0,0 +1,109 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +typedef SimpleAFDialogAction = (String, void Function(BuildContext)?); + +/// A simple dialog with a title, content, and actions. +/// +/// The primary button is a filled button and colored using theme or destructive +/// color depending on the [isDestructive] parameter. The secondary button is an +/// outlined button. +/// +Future showSimpleAFDialog({ + required BuildContext context, + required String title, + required String content, + bool isDestructive = false, + required SimpleAFDialogAction primaryAction, + SimpleAFDialogAction? secondaryAction, + bool barrierDismissible = true, +}) { + final theme = AppFlowyTheme.of(context); + + return showDialog( + context: context, + barrierColor: theme.surfaceColorScheme.overlay, + barrierDismissible: barrierDismissible, + builder: (_) { + return AFModal( + constraints: BoxConstraints( + maxWidth: AFModalDimension.S, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AFModalHeader( + leading: Text( + title, + style: theme.textStyle.heading4.standard( + color: theme.textColorScheme.primary, + ), + ), + trailing: [ + AFGhostButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.close_s, + size: Size.square(20), + ); + }, + ), + ], + ), + Flexible( + child: ConstrainedBox( + // AFModalDimension.dialogHeight - header - footer + constraints: BoxConstraints(minHeight: 108.0), + child: AFModalBody( + child: Text(content), + ), + ), + ), + AFModalFooter( + trailing: [ + if (secondaryAction != null) + AFOutlinedButton.normal( + onTap: () { + secondaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text(secondaryAction.$1); + }, + ), + isDestructive + ? AFFilledButton.destructive( + onTap: () { + primaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text( + primaryAction.$1, + style: TextStyle( + color: AppFlowyTheme.of(context) + .textColorScheme + .onFill, + ), + ); + }, + ) + : AFFilledButton.primary( + onTap: () { + primaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text(primaryAction.$1); + }, + ), + ], + ), + ], + ), + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 41548abd08..1dfc60b314 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -1,8 +1,10 @@ import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:appflowy_ui_example/src/buttons/buttons_page.dart'; -import 'package:appflowy_ui_example/src/textfield/textfield_page.dart'; import 'package:flutter/material.dart'; +import 'src/buttons/buttons_page.dart'; +import 'src/modal/modal_page.dart'; +import 'src/textfield/textfield_page.dart'; + enum ThemeMode { light, dark, @@ -60,6 +62,7 @@ class _MyHomePageState extends State { final tabs = [ Tab(text: 'Button'), Tab(text: 'TextField'), + Tab(text: 'Modal'), ]; @override @@ -92,6 +95,7 @@ class _MyHomePageState extends State { children: [ ButtonsPage(), TextFieldPage(), + ModalPage(), ], ), bottomNavigationBar: TabBar( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart new file mode 100644 index 0000000000..4a9480d1b9 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart @@ -0,0 +1,153 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class ModalPage extends StatefulWidget { + const ModalPage({super.key}); + + @override + State createState() => _ModalPageState(); +} + +class _ModalPageState extends State { + double width = AFModalDimension.M; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: Container( + constraints: BoxConstraints(maxWidth: 600), + padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl), + child: Column( + spacing: theme.spacing.l, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: theme.spacing.m, + mainAxisSize: MainAxisSize.min, + children: [ + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.S), + builder: (context, isHovering, disabled) { + return Text( + 'S', + style: TextStyle( + color: width == AFModalDimension.S + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.M), + builder: (context, isHovering, disabled) { + return Text( + 'M', + style: TextStyle( + color: width == AFModalDimension.M + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.L), + builder: (context, isHovering, disabled) { + return Text( + 'L', + style: TextStyle( + color: width == AFModalDimension.L + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + ], + ), + AFFilledButton.primary( + builder: (context, isHovering, disabled) { + return Text( + 'Show Modal', + style: TextStyle( + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + ); + }, + onTap: () { + showDialog( + context: context, + barrierColor: theme.surfaceColorScheme.overlay, + builder: (context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: AFModal( + constraints: BoxConstraints( + maxWidth: width, + maxHeight: AFModalDimension.dialogHeight, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AFModalHeader( + leading: Text( + 'Header', + style: theme.textStyle.heading4.standard( + color: theme.textColorScheme.primary, + ), + ), + trailing: [ + AFGhostButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return const Icon(Icons.close); + }, + ) + ], + ), + Expanded( + child: AFModalBody( + child: Text( + 'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'), + ), + ), + AFModalFooter( + trailing: [ + AFOutlinedButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return const Text('Cancel'); + }, + ), + AFFilledButton.primary( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return Text( + 'Apply', + style: TextStyle( + color: AppFlowyTheme.of(context) + .textColorScheme + .onFill, + ), + ); + }, + ), + ], + ) + ], + )), + ); + }, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart index b62f2cce9b..9bb36507e8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -144,10 +144,7 @@ class _AFBaseButtonState extends State { } if (isFocused) { - return AppFlowyTheme.of(context) - .borderColorScheme - .themeThick - .withAlpha(128); + return theme.borderColorScheme.themeThick.withAlpha(128); } return theme.borderColorScheme.transparent; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart index d01d64109c..584d50c07b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart @@ -1,2 +1,3 @@ export 'button/button.dart'; +export 'modal/modal.dart'; export 'textfield/textfield.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart new file mode 100644 index 0000000000..72a7dbb5cf --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart @@ -0,0 +1,9 @@ +class AFModalDimension { + const AFModalDimension._(); + + static const double S = 400.0; + static const double M = 560.0; + static const double L = 720.0; + + static const double dialogHeight = 200.0; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart new file mode 100644 index 0000000000..4b40aebcbd --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart @@ -0,0 +1,125 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +export 'dimension.dart'; + +class AFModal extends StatelessWidget { + const AFModal({ + super.key, + this.constraints = const BoxConstraints(), + required this.child, + }); + + final BoxConstraints constraints; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: Padding( + padding: EdgeInsets.all(theme.spacing.xl), + child: ConstrainedBox( + constraints: constraints, + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: theme.shadow.medium, + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + color: theme.surfaceColorScheme.primary, + ), + child: Material( + color: Colors.transparent, + child: child, + ), + ), + ), + ), + ); + } +} + +class AFModalHeader extends StatelessWidget { + const AFModalHeader({ + super.key, + required this.leading, + this.trailing = const [], + }); + + final Widget leading; + final List trailing; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.only( + top: theme.spacing.xl, + left: theme.spacing.xxl, + right: theme.spacing.xxl, + ), + child: Row( + spacing: theme.spacing.s, + children: [ + Expanded(child: leading), + ...trailing, + ], + ), + ); + } +} + +class AFModalFooter extends StatelessWidget { + const AFModalFooter({ + super.key, + this.leading = const [], + this.trailing = const [], + }); + + final List leading; + final List trailing; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.only( + bottom: theme.spacing.xl, + left: theme.spacing.xxl, + right: theme.spacing.xxl, + ), + child: Row( + spacing: theme.spacing.l, + children: [ + ...leading, + Spacer(), + ...trailing, + ], + ), + ); + } +} + +class AFModalBody extends StatelessWidget { + const AFModalBody({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.symmetric( + vertical: theme.spacing.l, + horizontal: theme.spacing.xxl, + ), + child: child, + ); + } +} From 889756ebb09272447aec8e00b7a7d4b29435d288 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:54:28 +0800 Subject: [PATCH 362/384] refactor: use script to generate design tokens (#7751) * refactor: use script to generate design tokens * chore: improve code readaility * refactor: make builder reusable to built in themes * chore: improve code readability --- .../ai/ai_writer_toolbar_item.dart | 4 +- .../link_embed/link_embed_menu.dart | 6 +- .../link_preview/custom_link_preview.dart | 2 +- .../custom_format_toolbar_items.dart | 2 +- .../custom_hightlight_color_toolbar_item.dart | 2 +- .../custom_link_toolbar_item.dart | 2 +- .../custom_text_align_toolbar_item.dart | 2 +- .../custom_text_color_toolbar_item.dart | 2 +- .../text_heading_toolbar_item.dart | 2 +- .../text_suggestions_toolbar_item.dart | 2 +- .../appflowy_ui/example/lib/main.dart | 2 +- .../src/component/textfield/textfield.dart | 4 +- .../lib/src/theme/appflowy_theme.dart | 21 +- .../theme/color_scheme/base/base_scheme.dart | 33 - .../lib/src/theme/color_scheme/base/blue.dart | 27 - .../src/theme/color_scheme/base/green.dart | 24 - .../src/theme/color_scheme/base/magenta.dart | 25 - .../src/theme/color_scheme/base/neutral.dart | 49 - .../src/theme/color_scheme/base/orange.dart | 25 - .../src/theme/color_scheme/base/purple.dart | 25 - .../lib/src/theme/color_scheme/base/red.dart | 27 - .../src/theme/color_scheme/base/subtle.dart | 265 ----- .../src/theme/color_scheme/base/yellow.dart | 24 - .../src/theme/color_scheme/border/border.dart | 49 - .../src/theme/color_scheme/color_scheme.dart | 8 - .../data/appflowy_default/primitive.dart | 298 +++++ .../theme/data/appflowy_default/semantic.dart | 377 ++++++ .../lib/src/theme/data/builder.dart | 323 +---- .../lib/src/theme/data/built_in_themes.dart | 1 + .../appflowy_ui/lib/src/theme/data/data.dart | 249 ---- .../lib/src/theme/definition/base_theme.dart | 33 + .../border_radius/border_radius.dart | 0 .../background_color_scheme.dart | 0 .../color_scheme}/border_color_scheme.dart | 0 .../color_scheme}/brand_color_scheme.dart | 0 .../definition/color_scheme/color_scheme.dart | 8 + .../color_scheme}/fill_color_scheme.dart | 0 .../color_scheme/icon_color_scheme.dart} | 4 +- .../color_scheme/other_color_scheme.dart | 9 + .../color_scheme}/surface_color_scheme.dart | 0 .../color_scheme}/text_color_scheme.dart | 0 .../theme/{ => definition}/shadow/shadow.dart | 0 .../{ => definition}/spacing/spacing.dart | 0 .../text_style/base/default_text_style.dart | 0 .../text_style/text_style.dart | 2 +- .../appflowy_ui/lib/src/theme/dimensions.dart | 17 - .../appflowy_ui/lib/src/theme/theme.dart | 13 +- .../script/Primitive.Mode 1.tokens.json | 984 ++++++++++++++++ .../script/Semantic.Dark Mode.tokens.json | 1039 +++++++++++++++++ .../script/Semantic.Light Mode.tokens.json | 1039 +++++++++++++++++ .../appflowy_ui/script/generate_theme.dart | 269 +++++ 51 files changed, 4116 insertions(+), 1183 deletions(-) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/border_radius/border_radius.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/background => definition/color_scheme}/background_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/border => definition/color_scheme}/border_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/brand => definition/color_scheme}/brand_color_scheme.dart (100%) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/fill => definition/color_scheme}/fill_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/icon/icon_color_theme.dart => definition/color_scheme/icon_color_scheme.dart} (86%) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/surface => definition/color_scheme}/surface_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/text => definition/color_scheme}/text_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/shadow/shadow.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/spacing/spacing.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/text_style/base/default_text_style.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/text_style/text_style.dart (88%) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index ab695b31a6..70d627d327 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -117,7 +117,7 @@ class _AiWriterToolbarActionListState extends State { } Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; final child = FlowyIconButton( width: 48, height: 32, @@ -186,7 +186,7 @@ class ImproveWritingButton extends StatelessWidget { icon: FlowySvg( FlowySvgs.toolbar_ai_improve_writing_m, size: Size.square(20.0), - color: theme.iconColorTheme.primary, + color: theme.iconColorScheme.primary, ), onPressed: () { if (_isAIWriterEnabled(editorState)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart index bddfcb9b54..c3d2aebbcc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart @@ -63,7 +63,7 @@ class _LinkEmbedMenuState extends State { Widget buildChild() { final theme = AppFlowyTheme.of(context), - iconScheme = theme.iconColorTheme, + iconScheme = theme.iconColorScheme, fillScheme = theme.fillColorScheme; return Container( @@ -102,7 +102,7 @@ class _LinkEmbedMenuState extends State { } Widget buildconvertBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; return AppFlowyPopover( offset: Offset(0, 6), direction: PopoverDirection.bottomWithRightAligned, @@ -170,7 +170,7 @@ class _LinkEmbedMenuState extends State { } Widget buildMoreOptionBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; return AppFlowyPopover( offset: Offset(0, 6), direction: PopoverDirection.bottomWithRightAligned, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 880a33b817..d7f3e26302 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -163,7 +163,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { Widget buildImage(BuildContext context) { final theme = AppFlowyTheme.of(context), fillScheme = theme.fillColorScheme, - iconScheme = theme.iconColorTheme; + iconScheme = theme.iconColorScheme; final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; Widget child; if (imageUrl?.isNotEmpty ?? false) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index 7dbf192bae..d4f3d21f46 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -82,7 +82,7 @@ class _FormatToolbarItem extends ToolbarItem { size: Size.square(20.0), color: (isDark && isHighlight) ? Color(0xFF282E3A) - : theme.iconColorTheme.primary, + : theme.iconColorScheme.primary, ), onPressed: () => editorState.toggleAttribute( name, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 2e115d240d..46f2c02c5a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -83,7 +83,7 @@ class _HighlightColorPickerWidgetState Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 36, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index cbbce9c943..8c9e6b69da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -45,7 +45,7 @@ final customLinkItem = ToolbarItem( size: Size.square(20.0), color: (isDark && isHref) ? Color(0xFF282E3A) - : theme.iconColorTheme.primary, + : theme.iconColorScheme.primary, ), onPressed: () { getIt().hideToolbar(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index 2a1688db19..efaff532f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -97,7 +97,7 @@ class _TextAlignActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 48, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 80f2d3138d..9f5a917b89 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -82,7 +82,7 @@ class _TextColorPickerWidgetState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 36, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 8140a7b7f3..5778b6b8a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -79,7 +79,7 @@ class _TextHeadingActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 48, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index 3c8f55caef..48f5d3f403 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -120,7 +120,7 @@ class _SuggestionsActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyHover( isSelected: () => isSelected, style: HoverStyle( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 1dfc60b314..c02001745d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -82,7 +82,7 @@ class _MyHomePageState extends State { actions: [ IconButton( icon: Icon( - theme.brightness == Brightness.light + Theme.of(context).brightness == Brightness.light ? Icons.dark_mode : Icons.light_mode, ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 3f5ad4cfed..9e61b71709 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -233,7 +233,7 @@ enum AFTextFieldSize { m, l; - EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { + EdgeInsetsGeometry contentPadding(AppFlowyBaseThemeData theme) { return EdgeInsets.symmetric( vertical: switch (this) { AFTextFieldSize.m => theme.spacing.s, @@ -243,7 +243,7 @@ enum AFTextFieldSize { ); } - BorderRadius borderRadius(AppFlowyThemeData theme) { + BorderRadius borderRadius(AppFlowyBaseThemeData theme) { return BorderRadius.circular( switch (this) { AFTextFieldSize.m => theme.borderRadius.m, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart index 49deecc178..8a99b737ec 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/data/data.dart'; +import 'package:appflowy_ui/src/theme/definition/base_theme.dart'; import 'package:flutter/widgets.dart'; class AppFlowyTheme extends StatelessWidget { @@ -8,10 +8,10 @@ class AppFlowyTheme extends StatelessWidget { required this.child, }); - final AppFlowyThemeData data; + final AppFlowyBaseThemeData data; final Widget child; - static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { + static AppFlowyBaseThemeData of(BuildContext context, {bool listen = true}) { final provider = maybeOf(context, listen: listen); if (provider == null) { throw FlutterError( @@ -26,27 +26,26 @@ class AppFlowyTheme extends StatelessWidget { return provider; } - static AppFlowyThemeData? maybeOf( + static AppFlowyBaseThemeData? maybeOf( BuildContext context, { bool listen = true, }) { if (listen) { return context .dependOnInheritedWidgetOfExactType() - ?.theme - .data; + ?.theme; } final provider = context .getElementForInheritedWidgetOfExactType() ?.widget; - return (provider as AppFlowyInheritedTheme?)?.theme.data; + return (provider as AppFlowyInheritedTheme?)?.theme; } @override Widget build(BuildContext context) { return AppFlowyInheritedTheme( - theme: this, + theme: data, child: child, ); } @@ -59,14 +58,14 @@ class AppFlowyInheritedTheme extends InheritedTheme { required super.child, }); - final AppFlowyTheme theme; + final AppFlowyBaseThemeData theme; @override Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: theme.data, child: child); + return AppFlowyTheme(data: theme, child: child); } @override bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - theme.data != oldWidget.theme.data; + theme != oldWidget.theme; } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart deleted file mode 100644 index 5b843d97e2..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:appflowy_ui/src/theme/color_scheme/base/blue.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/green.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/magenta.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/neutral.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/orange.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/purple.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/red.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/subtle.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/yellow.dart'; - -class AppFlowyBaseColorScheme { - const AppFlowyBaseColorScheme({ - this.blue = const BlueColors(), - this.green = const GreenColors(), - this.yellow = const YellowColors(), - this.red = const RedColors(), - this.orange = const OrangeColors(), - this.magenta = const MagentaColors(), - this.purple = const PurpleColors(), - this.neutral = const NeutralColors(), - this.subtle = const SubtleColors(), - }); - - final BlueColors blue; - final GreenColors green; - final YellowColors yellow; - final RedColors red; - final OrangeColors orange; - final MagentaColors magenta; - final PurpleColors purple; - final NeutralColors neutral; - final SubtleColors subtle; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart deleted file mode 100644 index fb73b42f60..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class BlueColors { - const BlueColors(); - - Color get blue100 => const Color(0xFFE3F6FF); - - Color get blue200 => const Color(0xFFA9E2FF); - - Color get blue300 => const Color(0xFF80D2FF); - - Color get blue400 => const Color(0xFF4EC1FF); - - Color get blue500 => const Color(0xFF00B5FF); - - Color get blue600 => const Color(0xFF0092D6); - - Color get blue700 => const Color(0xFF0078C0); - - Color get blue800 => const Color(0xFF0065A9); - - Color get blue900 => const Color(0xFF00508F); - - Color get blue1000 => const Color(0xFF003C77); - - Color get alphaBlue50015 => const Color(0x2600B5FF); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart deleted file mode 100644 index 652f5f5932..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class GreenColors { - const GreenColors(); - Color get green100 => const Color(0xFFECF9F5); - - Color get green200 => const Color(0xFFC3E5D8); - - Color get green300 => const Color(0xFF9AD1BC); - - Color get green400 => const Color(0xFF71BD9F); - - Color get green500 => const Color(0xFF48A982); - - Color get green600 => const Color(0xFF248569); - - Color get green700 => const Color(0xFF29725D); - - Color get green800 => const Color(0xFF2E6050); - - Color get green900 => const Color(0xFF305548); - - Color get green1000 => const Color(0xFF305244); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart deleted file mode 100644 index dec5617c67..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class MagentaColors { - const MagentaColors(); - - Color get magenta100 => const Color(0xFFFFE5EF); - - Color get magenta200 => const Color(0xFFFFB8D1); - - Color get magenta300 => const Color(0xFFFF8AB2); - - Color get magenta400 => const Color(0xFFFF5C93); - - Color get magenta500 => const Color(0xFFFB006D); - - Color get magenta600 => const Color(0xFFD2005F); - - Color get magenta700 => const Color(0xFFD2005F); - - Color get magenta800 => const Color(0xFF850040); - - Color get magenta900 => const Color(0xFF610031); - - Color get magenta1000 => const Color(0xFF400022); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart deleted file mode 100644 index 4b6c08b595..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class NeutralColors { - const NeutralColors(); - - Color get neutral100 => const Color(0xFFF8FAFF); - - Color get neutral200 => const Color(0xFFE4E8F5); - - Color get neutral300 => const Color(0xFFCED3E6); - - Color get neutral400 => const Color(0xFFB5BBD3); - - Color get neutral500 => const Color(0xFF989EB7); - - Color get neutral600 => const Color(0xFF6F748C); - - Color get neutral700 => const Color(0xFF54596E); - - Color get neutral800 => const Color(0xFF3C3F4E); - - Color get neutral900 => const Color(0xFF272930); - - Color get neutral1000 => const Color(0xFF21232A); - - Color get black => const Color(0xFF000000); - - Color get alphaBlack60 => const Color(0x99000000); - - Color get white => const Color(0xFFFFFFFF); - - Color get alphaWhite0 => const Color(0x00FFFFFF); - - Color get alphaWhite20 => const Color(0x33FFFFFF); - - Color get alphaWhite30 => const Color(0x4DFFFFFF); - - Color get alphaGrey10005 => const Color(0x0DF9FAFD); - - Color get alphaGrey10010 => const Color(0x1AF9FAFD); - - Color get alphaGrey100005 => const Color(0x0D1F2329); - - Color get alphaGrey100010 => const Color(0x1A1F2329); - - Color get alphaGrey100070 => const Color(0xB21F2329); - - Color get alphaGrey100080 => const Color(0xCC1F2329); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart deleted file mode 100644 index c9424bd960..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class OrangeColors { - const OrangeColors(); - - Color get orange100 => const Color(0xFFFFF3D5); - - Color get orange200 => const Color(0xFFFFE4AB); - - Color get orange300 => const Color(0xFFFFD181); - - Color get orange400 => const Color(0xFFFFBE62); - - Color get orange500 => const Color(0xFFFFA02E); - - Color get orange600 => const Color(0xFFDB7E21); - - Color get orange700 => const Color(0xFFB75F17); - - Color get orange800 => const Color(0xFF93450E); - - Color get orange900 => const Color(0xFF7A3108); - - Color get orange1000 => const Color(0xFF602706); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart deleted file mode 100644 index fa3b9e54cf..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class PurpleColors { - const PurpleColors(); - - Color get purple100 => const Color(0xFFF1E0FF); - - Color get purple200 => const Color(0xFFE1B3FF); - - Color get purple300 => const Color(0xFFD185FF); - - Color get purple400 => const Color(0xFFBC58FF); - - Color get purple500 => const Color(0xFF9327FF); - - Color get purple600 => const Color(0xFF7A1DCC); - - Color get purple700 => const Color(0xFF6617B3); - - Color get purple800 => const Color(0xFF55138F); - - Color get purple900 => const Color(0xFF470C72); - - Color get purple1000 => const Color(0xFF380758); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart deleted file mode 100644 index 030f2988bc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class RedColors { - const RedColors(); - - Color get red100 => const Color(0xFFFFD2DD); - - Color get red200 => const Color(0xFFFFA5B4); - - Color get red300 => const Color(0xFFFF7D87); - - Color get red400 => const Color(0xFFFF5050); - - Color get red500 => const Color(0xFFF33641); - - Color get red600 => const Color(0xFFE71D32); - - Color get red700 => const Color(0xFFAD1625); - - Color get red800 => const Color(0xFF8C101C); - - Color get red900 => const Color(0xFF6E0A1E); - - Color get red1000 => const Color(0xFF4C0A17); - - Color get alphaRed50010 => const Color(0x1AF33641); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart deleted file mode 100644 index 2fddd14f1a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'package:flutter/material.dart'; - -class SubtleColors { - const SubtleColors(); - - // Rose colors - Color get rose100 => const Color(0xFFFCF2F2); - - Color get rose200 => const Color(0xFFFAE3E3); - - Color get rose300 => const Color(0xFFFAD9D9); - - Color get rose400 => const Color(0xFFEDADAD); - - Color get rose500 => const Color(0xFFCC4E4E); - - Color get rose600 => const Color(0xFF702828); - - // Papaya colors - Color get papaya100 => const Color(0xFFFCF4F0); - - Color get papaya200 => const Color(0xFFFAE8DE); - - Color get papaya300 => const Color(0xFFFADFD2); - - Color get papaya400 => const Color(0xFFF0BDA3); - - Color get papaya500 => const Color(0xFFD67240); - - Color get papaya600 => const Color(0xFF6B3215); - - // Tangerine colors - Color get tangerine100 => const Color(0xFFFFF7ED); - - Color get tangerine200 => const Color(0xFFFCEDD9); - - Color get tangerine300 => const Color(0xFFFAE5CA); - - Color get tangerine400 => const Color(0xFFF2CB99); - - Color get tangerine500 => const Color(0xFFDB8F2C); - - Color get tangerine600 => const Color(0xFF613B0A); - - // Mango colors - Color get mango100 => const Color(0xFFFFF9EC); - - Color get mango200 => const Color(0xFFFCF1D7); - - Color get mango300 => const Color(0xFFFAE9C3); - - Color get mango400 => const Color(0xFFF5D68E); - - Color get mango500 => const Color(0xFFE0A416); - - Color get mango600 => const Color(0xFF5C4102); - - // Lemon colors - Color get lemon100 => const Color(0xFFFFFBE8); - - Color get lemon200 => const Color(0xFFFCF5CF); - - Color get lemon300 => const Color(0xFFFAEFB9); - - Color get lemon400 => const Color(0xFFF5E282); - - Color get lemon500 => const Color(0xFFE0BB00); - - Color get lemon600 => const Color(0xFF574800); - - // Olive colors - Color get olive100 => const Color(0xFFF9FAE6); - - Color get olive200 => const Color(0xFFF6F7D0); - - Color get olive300 => const Color(0xFFF0F2B3); - - Color get olive400 => const Color(0xFFDBDE83); - - Color get olive500 => const Color(0xFFADB204); - - Color get olive600 => const Color(0xFF4A4C03); - - // Lime colors - Color get lime100 => const Color(0xFFF6F9E6); - - Color get lime200 => const Color(0xFFEEF5CE); - - Color get lime300 => const Color(0xFFE7F0BB); - - Color get lime400 => const Color(0xFFCFDB91); - - Color get lime500 => const Color(0xFF92A822); - - Color get lime600 => const Color(0xFF414D05); - - // Grass colors - Color get grass100 => const Color(0xFFF4FAEB); - - Color get grass200 => const Color(0xFFE9F5D7); - - Color get grass300 => const Color(0xFFDEF0C5); - - Color get grass400 => const Color(0xFFBFD998); - - Color get grass500 => const Color(0xFF75A828); - - Color get grass600 => const Color(0xFF334D0C); - - // Forest colors - Color get forest100 => const Color(0xFFF1FAF0); - - Color get forest200 => const Color(0xFFE2F5DF); - - Color get forest300 => const Color(0xFFD7F0D3); - - Color get forest400 => const Color(0xFFA8D6A1); - - Color get forest500 => const Color(0xFF49A33B); - - Color get forest600 => const Color(0xFF1E4F16); - - // Jade colors - Color get jade100 => const Color(0xFFF0FAF6); - - Color get jade200 => const Color(0xFFDFF5EB); - - Color get jade300 => const Color(0xFFCEF0E1); - - Color get jade400 => const Color(0xFF90D1B5); - - Color get jade500 => const Color(0xFF1C9963); - - Color get jade600 => const Color(0xFF075231); - - // Aqua colors - Color get aqua100 => const Color(0xFFF0F9FA); - - Color get aqua200 => const Color(0xFFDFF3F5); - - Color get aqua300 => const Color(0xFFCCECF0); - - Color get aqua400 => const Color(0xFF83CCD4); - - Color get aqua500 => const Color(0xFF008E9E); - - Color get aqua600 => const Color(0xFF004E57); - - // Azure colors - Color get azure100 => const Color(0xFFF0F6FA); - - Color get azure200 => const Color(0xFFE1EEF7); - - Color get azure300 => const Color(0xFFD3E6F5); - - Color get azure400 => const Color(0xFF88C0EB); - - Color get azure500 => const Color(0xFF0877CC); - - Color get azure600 => const Color(0xFF154469); - - // Denim colors - Color get denim100 => const Color(0xFFF0F3FA); - - Color get denim200 => const Color(0xFFE3EBFA); - - Color get denim300 => const Color(0xFFD7E2F7); - - Color get denim400 => const Color(0xFF9AB6ED); - - Color get denim500 => const Color(0xFF3267D1); - - Color get denim600 => const Color(0xFF223C70); - - // Mauve colors - Color get mauve100 => const Color(0xFFF2F2FC); - - Color get mauve200 => const Color(0xFFE6E6FA); - - Color get mauve300 => const Color(0xFFDCDCF7); - - Color get mauve400 => const Color(0xFFAEAEF5); - - Color get mauve500 => const Color(0xFF5555E0); - - Color get mauve600 => const Color(0xFF36366B); - - // Lavender colors - Color get lavender100 => const Color(0xFFF6F3FC); - - Color get lavender200 => const Color(0xFFEBE3FA); - - Color get lavender300 => const Color(0xFFE4DAF7); - - Color get lavender400 => const Color(0xFFC1AAF0); - - Color get lavender500 => const Color(0xFF8153DB); - - Color get lavender600 => const Color(0xFF462F75); - - // Lilac colors - Color get lilac100 => const Color(0xFFF7F0FA); - - Color get lilac200 => const Color(0xFFF0E1F7); - - Color get lilac300 => const Color(0xFFEDD7F7); - - Color get lilac400 => const Color(0xFFD3A9E8); - - Color get lilac500 => const Color(0xFF9E4CC7); - - Color get lilac600 => const Color(0xFF562D6B); - - // Mallow colors - Color get mallow100 => const Color(0xFFFAF0FA); - - Color get mallow200 => const Color(0xFFF5E1F4); - - Color get mallow300 => const Color(0xFFF5D7F4); - - Color get mallow400 => const Color(0xFFDEA4DC); - - Color get mallow500 => const Color(0xFFB240AF); - - Color get mallow600 => const Color(0xFF632861); - - // Camellia colors - Color get camellia100 => const Color(0xFFF9EFF3); - - Color get camellia200 => const Color(0xFFF7E1EB); - - Color get camellia300 => const Color(0xFFF7D7E5); - - Color get camellia400 => const Color(0xFFE5A3C0); - - Color get camellia500 => const Color(0xFFC24279); - - Color get camellia600 => const Color(0xFF6E2343); - - // Smoke colors - Color get smoke100 => const Color(0xFFF5F5F5); - - Color get smoke200 => const Color(0xFFE8E8E8); - - Color get smoke300 => const Color(0xFFDEDEDE); - - Color get smoke400 => const Color(0xFFB8B8B8); - - Color get smoke500 => const Color(0xFF6E6E6E); - - Color get smoke600 => const Color(0xFF404040); - - // Iron colors - Color get iron100 => const Color(0xFFF2F4F7); - - Color get iron200 => const Color(0xFFE6E9F0); - - Color get iron300 => const Color(0xFFDADEE5); - - Color get iron400 => const Color(0xFFB0B5BF); - - Color get iron500 => const Color(0xFF666F80); - - Color get iron600 => const Color(0xFF394152); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart deleted file mode 100644 index a38cf2bd78..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class YellowColors { - const YellowColors(); - Color get yellow100 => const Color(0xFFFFF9B2); - - Color get yellow200 => const Color(0xFFFFEC66); - - Color get yellow300 => const Color(0xFFFFDF1A); - - Color get yellow400 => const Color(0xFFFFCC00); - - Color get yellow500 => const Color(0xFFFFCE00); - - Color get yellow600 => const Color(0xFFE6B800); - - Color get yellow700 => const Color(0xFFCC9F00); - - Color get yellow800 => const Color(0xFFB38A00); - - Color get yellow900 => const Color(0xFF9A7500); - - Color get yellow1000 => const Color(0xFF7F6200); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart deleted file mode 100644 index d1618b6cff..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBorderColorScheme { - const AppFlowyBorderColorScheme({ - required this.greyPrimary, - required this.greyPrimaryHover, - required this.greySecondary, - required this.greySecondaryHover, - required this.greyTertiary, - required this.greyTertiaryHover, - required this.greyQuaternary, - required this.greyQuaternaryHover, - required this.transparent, - required this.themeThick, - required this.themeThickHover, - required this.infoThick, - required this.infoThickHover, - required this.successThick, - required this.successThickHover, - required this.warningThick, - required this.warningThickHover, - required this.errorThick, - required this.errorThickHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color greyPrimary; - final Color greyPrimaryHover; - final Color greySecondary; - final Color greySecondaryHover; - final Color greyTertiary; - final Color greyTertiaryHover; - final Color greyQuaternary; - final Color greyQuaternaryHover; - final Color transparent; - final Color themeThick; - final Color themeThickHover; - final Color infoThick; - final Color infoThickHover; - final Color successThick; - final Color successThickHover; - final Color warningThick; - final Color warningThickHover; - final Color errorThick; - final Color errorThickHover; - final Color purpleThick; - final Color purpleThickHover; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart deleted file mode 100644 index 5a1e7debeb..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'background/background_color_scheme.dart'; -export 'base/base_scheme.dart'; -export 'border/border_color_scheme.dart'; -export 'brand/brand_color_scheme.dart'; -export 'fill/fill_color_scheme.dart'; -export 'icon/icon_color_theme.dart'; -export 'surface/surface_color_scheme.dart'; -export 'text/text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart new file mode 100644 index 0000000000..1eee06953f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -0,0 +1,298 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: 2025-04-15T23:05:39.278903 +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:flutter/material.dart'; + +class AppFlowyPrimitiveTokens { + AppFlowyPrimitiveTokens._(); + + /// #f8faff + static Color get neutral100 => Color(0xFFF8FAFF); + + /// #e4e8f5 + static Color get neutral200 => Color(0xFFE4E8F5); + + /// #ced3e6 + static Color get neutral300 => Color(0xFFCED3E6); + + /// #b5bbd3 + static Color get neutral400 => Color(0xFFB5BBD3); + + /// #989eb7 + static Color get neutral500 => Color(0xFF989EB7); + + /// #6f748c + static Color get neutral600 => Color(0xFF6F748C); + + /// #54596e + static Color get neutral700 => Color(0xFF54596E); + + /// #3c3f4e + static Color get neutral800 => Color(0xFF3C3F4E); + + /// #272930 + static Color get neutral900 => Color(0xFF272930); + + /// #21232a + static Color get neutral1000 => Color(0xFF21232A); + + /// #000000 + static Color get neutralBlack => Color(0xFF000000); + + /// #00000099 + static Color get neutralAlphaBlack60 => Color(0x99000000); + + /// #ffffff + static Color get neutralWhite => Color(0xFFFFFFFF); + + /// #ffffff00 + static Color get neutralAlphaWhite0 => Color(0x00FFFFFF); + + /// #ffffff33 + static Color get neutralAlphaWhite20 => Color(0x33FFFFFF); + + /// #ffffff4d + static Color get neutralAlphaWhite30 => Color(0x4DFFFFFF); + + /// #f9fafd0d + static Color get neutralAlphaGrey10005 => Color(0x0DF9FAFD); + + /// #f9fafd1a + static Color get neutralAlphaGrey10010 => Color(0x1AF9FAFD); + + /// #1f23290d + static Color get neutralAlphaGrey100005 => Color(0x0D1F2329); + + /// #1f23291a + static Color get neutralAlphaGrey100010 => Color(0x1A1F2329); + + /// #1f2329b2 + static Color get neutralAlphaGrey100070 => Color(0xB21F2329); + + /// #1f2329cc + static Color get neutralAlphaGrey100080 => Color(0xCC1F2329); + + /// #e3f6ff + static Color get blue100 => Color(0xFFE3F6FF); + + /// #a9e2ff + static Color get blue200 => Color(0xFFA9E2FF); + + /// #80d2ff + static Color get blue300 => Color(0xFF80D2FF); + + /// #4ec1ff + static Color get blue400 => Color(0xFF4EC1FF); + + /// #00b5ff + static Color get blue500 => Color(0xFF00B5FF); + + /// #0092d6 + static Color get blue600 => Color(0xFF0092D6); + + /// #0078c0 + static Color get blue700 => Color(0xFF0078C0); + + /// #0065a9 + static Color get blue800 => Color(0xFF0065A9); + + /// #00508f + static Color get blue900 => Color(0xFF00508F); + + /// #003c77 + static Color get blue1000 => Color(0xFF003C77); + + /// #00b5ff26 + static Color get blueAlphaBlue50015 => Color(0x2600B5FF); + + /// #ecf9f5 + static Color get green100 => Color(0xFFECF9F5); + + /// #c3e5d8 + static Color get green200 => Color(0xFFC3E5D8); + + /// #9ad1bc + static Color get green300 => Color(0xFF9AD1BC); + + /// #71bd9f + static Color get green400 => Color(0xFF71BD9F); + + /// #48a982 + static Color get green500 => Color(0xFF48A982); + + /// #248569 + static Color get green600 => Color(0xFF248569); + + /// #29725d + static Color get green700 => Color(0xFF29725D); + + /// #2e6050 + static Color get green800 => Color(0xFF2E6050); + + /// #305548 + static Color get green900 => Color(0xFF305548); + + /// #305244 + static Color get green1000 => Color(0xFF305244); + + /// #f1e0ff + static Color get purple100 => Color(0xFFF1E0FF); + + /// #e1b3ff + static Color get purple200 => Color(0xFFE1B3FF); + + /// #d185ff + static Color get purple300 => Color(0xFFD185FF); + + /// #bc58ff + static Color get purple400 => Color(0xFFBC58FF); + + /// #9327ff + static Color get purple500 => Color(0xFF9327FF); + + /// #7a1dcc + static Color get purple600 => Color(0xFF7A1DCC); + + /// #6617b3 + static Color get purple700 => Color(0xFF6617B3); + + /// #55138f + static Color get purple800 => Color(0xFF55138F); + + /// #470c72 + static Color get purple900 => Color(0xFF470C72); + + /// #380758 + static Color get purple1000 => Color(0xFF380758); + + /// #ffe5ef + static Color get magenta100 => Color(0xFFFFE5EF); + + /// #ffb8d1 + static Color get magenta200 => Color(0xFFFFB8D1); + + /// #ff8ab2 + static Color get magenta300 => Color(0xFFFF8AB2); + + /// #ff5c93 + static Color get magenta400 => Color(0xFFFF5C93); + + /// #fb006d + static Color get magenta500 => Color(0xFFFB006D); + + /// #d2005f + static Color get magenta600 => Color(0xFFD2005F); + + /// #d2005f + static Color get magenta700 => Color(0xFFD2005F); + + /// #850040 + static Color get magenta800 => Color(0xFF850040); + + /// #610031 + static Color get magenta900 => Color(0xFF610031); + + /// #400022 + static Color get magenta1000 => Color(0xFF400022); + + /// #ffd2dd + static Color get red100 => Color(0xFFFFD2DD); + + /// #ffa5b4 + static Color get red200 => Color(0xFFFFA5B4); + + /// #ff7d87 + static Color get red300 => Color(0xFFFF7D87); + + /// #ff5050 + static Color get red400 => Color(0xFFFF5050); + + /// #f33641 + static Color get red500 => Color(0xFFF33641); + + /// #e71d32 + static Color get red600 => Color(0xFFE71D32); + + /// #ad1625 + static Color get red700 => Color(0xFFAD1625); + + /// #8c101c + static Color get red800 => Color(0xFF8C101C); + + /// #6e0a1e + static Color get red900 => Color(0xFF6E0A1E); + + /// #4c0a17 + static Color get red1000 => Color(0xFF4C0A17); + + /// #f336411a + static Color get redAlphaRed50010 => Color(0x1AF33641); + + /// #fff3d5 + static Color get orange100 => Color(0xFFFFF3D5); + + /// #ffe4ab + static Color get orange200 => Color(0xFFFFE4AB); + + /// #ffd181 + static Color get orange300 => Color(0xFFFFD181); + + /// #ffbe62 + static Color get orange400 => Color(0xFFFFBE62); + + /// #ffa02e + static Color get orange500 => Color(0xFFFFA02E); + + /// #db7e21 + static Color get orange600 => Color(0xFFDB7E21); + + /// #b75f17 + static Color get orange700 => Color(0xFFB75F17); + + /// #93450e + static Color get orange800 => Color(0xFF93450E); + + /// #7a3108 + static Color get orange900 => Color(0xFF7A3108); + + /// #602706 + static Color get orange1000 => Color(0xFF602706); + + /// #fff9b2 + static Color get yellow100 => Color(0xFFFFF9B2); + + /// #ffec66 + static Color get yellow200 => Color(0xFFFFEC66); + + /// #ffdf1a + static Color get yellow300 => Color(0xFFFFDF1A); + + /// #ffcc00 + static Color get yellow400 => Color(0xFFFFCC00); + + /// #ffce00 + static Color get yellow500 => Color(0xFFFFCE00); + + /// #e6b800 + static Color get yellow600 => Color(0xFFE6B800); + + /// #cc9f00 + static Color get yellow700 => Color(0xFFCC9F00); + + /// #b38a00 + static Color get yellow800 => Color(0xFFB38A00); + + /// #9a7500 + static Color get yellow900 => Color(0xFF9A7500); + + /// #7f6200 + static Color get yellow1000 => Color(0xFF7F6200); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart new file mode 100644 index 0000000000..08206a6572 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -0,0 +1,377 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: 2025-04-15T23:05:39.288085 +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +import '../builder.dart'; +import 'primitive.dart'; + +class AppFlowyThemeData implements AppFlowyBaseThemeData { + const AppFlowyThemeData._({ + required this.textStyle, + required this.textColorScheme, + required this.borderColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.iconColorScheme, + required this.backgroundColorScheme, + required this.otherColorsColorScheme, + }); + + factory AppFlowyThemeData.light() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.light); + + final textColorScheme = AppFlowyTextColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral600, + tertiary: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral200, + inverse: AppFlowyPrimitiveTokens.neutralWhite, + onFill: AppFlowyPrimitiveTokens.neutralWhite, + theme: AppFlowyPrimitiveTokens.blue500, + themeHover: AppFlowyPrimitiveTokens.blue600, + action: AppFlowyPrimitiveTokens.blue500, + actionHover: AppFlowyPrimitiveTokens.blue600, + info: AppFlowyPrimitiveTokens.blue500, + infoHover: AppFlowyPrimitiveTokens.blue600, + success: AppFlowyPrimitiveTokens.green600, + successHover: AppFlowyPrimitiveTokens.green700, + warning: AppFlowyPrimitiveTokens.orange600, + warningHover: AppFlowyPrimitiveTokens.orange700, + error: AppFlowyPrimitiveTokens.red600, + errorHover: AppFlowyPrimitiveTokens.red700, + purple: AppFlowyPrimitiveTokens.purple500, + purpleHover: AppFlowyPrimitiveTokens.purple600, + ); + + final iconColorScheme = AppFlowyIconColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral600, + tertiary: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral200, + white: AppFlowyPrimitiveTokens.neutralWhite, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final borderColorScheme = AppFlowyBorderColorScheme( + greyPrimary: AppFlowyPrimitiveTokens.neutral1000, + greyPrimaryHover: AppFlowyPrimitiveTokens.neutral900, + greySecondary: AppFlowyPrimitiveTokens.neutral800, + greySecondaryHover: AppFlowyPrimitiveTokens.neutral700, + greyTertiary: AppFlowyPrimitiveTokens.neutral300, + greyTertiaryHover: AppFlowyPrimitiveTokens.neutral400, + greyQuaternary: AppFlowyPrimitiveTokens.neutral100, + greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral200, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red700, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final fillColorScheme = AppFlowyFillColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + primaryHover: AppFlowyPrimitiveTokens.neutral900, + secondary: AppFlowyPrimitiveTokens.neutral600, + secondaryHover: AppFlowyPrimitiveTokens.neutral500, + tertiary: AppFlowyPrimitiveTokens.neutral300, + tertiaryHover: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral100, + quaternaryHover: AppFlowyPrimitiveTokens.neutral200, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey100005, + primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100010, + primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, + primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, + white: AppFlowyPrimitiveTokens.neutralWhite, + whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, + whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, + black: AppFlowyPrimitiveTokens.neutralBlack, + themeLight: AppFlowyPrimitiveTokens.blue100, + themeLightHover: AppFlowyPrimitiveTokens.blue200, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, + infoLight: AppFlowyPrimitiveTokens.blue100, + infoLightHover: AppFlowyPrimitiveTokens.blue200, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successLight: AppFlowyPrimitiveTokens.green100, + successLightHover: AppFlowyPrimitiveTokens.green200, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningLight: AppFlowyPrimitiveTokens.orange100, + warningLightHover: AppFlowyPrimitiveTokens.orange200, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorLight: AppFlowyPrimitiveTokens.red100, + errorLightHover: AppFlowyPrimitiveTokens.red200, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red700, + errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, + purpleLight: AppFlowyPrimitiveTokens.purple100, + purpleLightHover: AppFlowyPrimitiveTokens.purple200, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + purpleThick: AppFlowyPrimitiveTokens.purple500, + ); + + final surfaceColorScheme = AppFlowySurfaceColorScheme( + primary: AppFlowyPrimitiveTokens.neutralWhite, + overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, + ); + + final backgroundColorScheme = AppFlowyBackgroundColorScheme( + primary: AppFlowyPrimitiveTokens.neutralWhite, + secondary: AppFlowyPrimitiveTokens.neutral100, + tertiary: AppFlowyPrimitiveTokens.neutral200, + quaternary: AppFlowyPrimitiveTokens.neutral300, + ); + + final brandColorScheme = AppFlowyBrandColorScheme( + skyline: Color(0xFF00B5FF), + aqua: Color(0xFF00C8FF), + violet: Color(0xFF9327FF), + amethyst: Color(0xFF8427E0), + berry: Color(0xFFE3006D), + coral: Color(0xFFFB006D), + golden: Color(0xFFF7931E), + amber: Color(0xFFFFBD00), + lemon: Color(0xFFFFCE00), + ); + + final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( + textHighlight: AppFlowyPrimitiveTokens.blue200, + ); + + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + } + + factory AppFlowyThemeData.dark() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.dark); + + final textColorScheme = AppFlowyTextColorScheme( + primary: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + inverse: AppFlowyPrimitiveTokens.neutral1000, + onFill: AppFlowyPrimitiveTokens.neutralWhite, + theme: AppFlowyPrimitiveTokens.blue500, + themeHover: AppFlowyPrimitiveTokens.blue600, + action: AppFlowyPrimitiveTokens.blue500, + actionHover: AppFlowyPrimitiveTokens.blue600, + info: AppFlowyPrimitiveTokens.blue500, + infoHover: AppFlowyPrimitiveTokens.blue600, + success: AppFlowyPrimitiveTokens.green600, + successHover: AppFlowyPrimitiveTokens.green700, + warning: AppFlowyPrimitiveTokens.orange600, + warningHover: AppFlowyPrimitiveTokens.orange700, + error: AppFlowyPrimitiveTokens.red500, + errorHover: AppFlowyPrimitiveTokens.red400, + purple: AppFlowyPrimitiveTokens.purple500, + purpleHover: AppFlowyPrimitiveTokens.purple600, + ); + + final iconColorScheme = AppFlowyIconColorScheme( + primary: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + white: AppFlowyPrimitiveTokens.neutralWhite, + purpleThick: Color(0xFFFFFFFF), + purpleThickHover: Color(0xFFFFFFFF), + ); + + final borderColorScheme = AppFlowyBorderColorScheme( + greyPrimary: AppFlowyPrimitiveTokens.neutral100, + greyPrimaryHover: AppFlowyPrimitiveTokens.neutral200, + greySecondary: AppFlowyPrimitiveTokens.neutral300, + greySecondaryHover: AppFlowyPrimitiveTokens.neutral400, + greyTertiary: AppFlowyPrimitiveTokens.neutral800, + greyTertiaryHover: AppFlowyPrimitiveTokens.neutral700, + greyQuaternary: AppFlowyPrimitiveTokens.neutral1000, + greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral900, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorThick: AppFlowyPrimitiveTokens.red500, + errorThickHover: AppFlowyPrimitiveTokens.red400, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final fillColorScheme = AppFlowyFillColorScheme( + primary: AppFlowyPrimitiveTokens.neutral100, + primaryHover: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral300, + secondaryHover: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + tertiaryHover: AppFlowyPrimitiveTokens.neutral500, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + quaternaryHover: AppFlowyPrimitiveTokens.neutral900, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey10005, + primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey10010, + primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, + primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, + white: AppFlowyPrimitiveTokens.neutralWhite, + whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, + whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, + black: AppFlowyPrimitiveTokens.neutralBlack, + themeLight: AppFlowyPrimitiveTokens.blue100, + themeLightHover: AppFlowyPrimitiveTokens.blue200, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue400, + themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, + infoLight: AppFlowyPrimitiveTokens.blue100, + infoLightHover: AppFlowyPrimitiveTokens.blue200, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successLight: AppFlowyPrimitiveTokens.green100, + successLightHover: AppFlowyPrimitiveTokens.green200, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningLight: AppFlowyPrimitiveTokens.orange100, + warningLightHover: AppFlowyPrimitiveTokens.orange200, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorLight: AppFlowyPrimitiveTokens.red100, + errorLightHover: AppFlowyPrimitiveTokens.red200, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red500, + errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, + purpleLight: AppFlowyPrimitiveTokens.purple100, + purpleLightHover: AppFlowyPrimitiveTokens.purple200, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + purpleThick: AppFlowyPrimitiveTokens.purple500, + ); + + final surfaceColorScheme = AppFlowySurfaceColorScheme( + primary: AppFlowyPrimitiveTokens.neutral900, + overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, + ); + + final backgroundColorScheme = AppFlowyBackgroundColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral900, + tertiary: AppFlowyPrimitiveTokens.neutral800, + quaternary: AppFlowyPrimitiveTokens.neutral700, + ); + + final brandColorScheme = AppFlowyBrandColorScheme( + skyline: Color(0xFF00B5FF), + aqua: Color(0xFF00C8FF), + violet: Color(0xFF9327FF), + amethyst: Color(0xFF8427E0), + berry: Color(0xFFE3006D), + coral: Color(0xFFFB006D), + golden: Color(0xFFF7931E), + amber: Color(0xFFFFBD00), + lemon: Color(0xFFFFCE00), + ); + + final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( + textHighlight: AppFlowyPrimitiveTokens.blue200, + ); + + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + } + + static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); + + @override + final AppFlowyBaseTextStyle textStyle; + + @override + final AppFlowyTextColorScheme textColorScheme; + + @override + final AppFlowyBorderColorScheme borderColorScheme; + + @override + final AppFlowyFillColorScheme fillColorScheme; + + @override + final AppFlowySurfaceColorScheme surfaceColorScheme; + + @override + final AppFlowyBorderRadius borderRadius; + + @override + final AppFlowySpacing spacing; + + @override + final AppFlowyShadow shadow; + + @override + final AppFlowyBrandColorScheme brandColorScheme; + + @override + final AppFlowyIconColorScheme iconColorScheme; + + @override + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + @override + final AppFlowyOtherColorsColorScheme otherColorsColorScheme; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 06c6eb5d8e..a4f83109cb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -1,305 +1,30 @@ -import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/dimensions.dart'; -import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; +import 'package:appflowy_ui/src/theme/definition/border_radius/border_radius.dart'; +import 'package:appflowy_ui/src/theme/definition/shadow/shadow.dart'; +import 'package:appflowy_ui/src/theme/definition/spacing/spacing.dart'; import 'package:flutter/material.dart'; +class AppFlowySpacingConstant { + static const double spacing100 = 4; + static const double spacing200 = 6; + static const double spacing300 = 8; + static const double spacing400 = 12; + static const double spacing500 = 16; + static const double spacing600 = 20; +} + +class AppFlowyBorderRadiusConstant { + static const double radius100 = 4; + static const double radius200 = 6; + static const double radius300 = 8; + static const double radius400 = 12; + static const double radius500 = 16; + static const double radius600 = 20; +} + class AppFlowyThemeBuilder { const AppFlowyThemeBuilder(); - AppFlowyTextColorScheme buildTextColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - inverse: colorScheme.neutral.white, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red600, - errorHover: colorScheme.red.red700, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - inverse: colorScheme.neutral.neutral1000, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red500, - errorHover: colorScheme.red.red400, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowyIconColorTheme buildIconColorTheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyIconColorTheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - white: colorScheme.neutral.white, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyIconColorTheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - white: colorScheme.neutral.white, - purpleThick: const Color(0xFFFFFFFF), - purpleThickHover: const Color(0xFFFFFFFF), - ), - }; - } - - AppFlowyBorderColorScheme buildBorderColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyBorderColorScheme( - greyPrimary: colorScheme.neutral.neutral1000, - greyPrimaryHover: colorScheme.neutral.neutral900, - greySecondary: colorScheme.neutral.neutral800, - greySecondaryHover: colorScheme.neutral.neutral700, - greyTertiary: colorScheme.neutral.neutral300, - greyTertiaryHover: colorScheme.neutral.neutral400, - greyQuaternary: colorScheme.neutral.neutral100, - greyQuaternaryHover: colorScheme.neutral.neutral200, - transparent: colorScheme.neutral.alphaWhite0, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyBorderColorScheme( - greyPrimary: colorScheme.neutral.neutral100, - greyPrimaryHover: colorScheme.neutral.neutral200, - greySecondary: colorScheme.neutral.neutral300, - greySecondaryHover: colorScheme.neutral.neutral400, - greyTertiary: colorScheme.neutral.neutral800, - greyTertiaryHover: colorScheme.neutral.neutral700, - greyQuaternary: colorScheme.neutral.neutral1000, - greyQuaternaryHover: colorScheme.neutral.neutral900, - transparent: colorScheme.neutral.alphaWhite0, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorThick: colorScheme.red.red500, - errorThickHover: colorScheme.red.red400, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowyFillColorScheme buildFillColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.dark => AppFlowyFillColorScheme( - primary: colorScheme.neutral.neutral100, - primaryHover: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral300, - secondaryHover: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - tertiaryHover: colorScheme.neutral.neutral500, - quaternary: colorScheme.neutral.neutral1000, - quaternaryHover: colorScheme.neutral.neutral900, - transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey10005, - primaryAlpha5Hover: colorScheme.neutral.alphaGrey10010, - primaryAlpha80: colorScheme.neutral.alphaGrey100080, - primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, - white: colorScheme.neutral.white, - whiteAlpha: colorScheme.neutral.alphaWhite20, - whiteAlphaHover: colorScheme.neutral.alphaWhite30, - black: colorScheme.neutral.black, - themeLight: colorScheme.blue.blue100, - themeLightHover: colorScheme.blue.blue200, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - themeSelect: colorScheme.blue.alphaBlue50015, - infoLight: colorScheme.blue.blue100, - infoLightHover: colorScheme.blue.blue200, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successLight: colorScheme.green.green100, - successLightHover: colorScheme.green.green200, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningLight: colorScheme.orange.orange100, - warningLightHover: colorScheme.orange.orange200, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorLight: colorScheme.red.red100, - errorLightHover: colorScheme.red.red200, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - errorSelect: colorScheme.red.alphaRed50010, - purpleLight: colorScheme.purple.purple100, - purpleLightHover: colorScheme.purple.purple200, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.light => AppFlowyFillColorScheme( - primary: colorScheme.neutral.neutral1000, - primaryHover: colorScheme.neutral.neutral900, - secondary: colorScheme.neutral.neutral600, - secondaryHover: colorScheme.neutral.neutral500, - tertiary: colorScheme.neutral.neutral300, - tertiaryHover: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral100, - quaternaryHover: colorScheme.neutral.neutral200, - transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey100005, - primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010, - primaryAlpha80: colorScheme.neutral.alphaGrey100080, - primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, - white: colorScheme.neutral.white, - whiteAlpha: colorScheme.neutral.alphaWhite20, - whiteAlphaHover: colorScheme.neutral.alphaWhite30, - black: colorScheme.neutral.black, - themeLight: colorScheme.blue.blue100, - themeLightHover: colorScheme.blue.blue200, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - themeSelect: colorScheme.blue.alphaBlue50015, - infoLight: colorScheme.blue.blue100, - infoLightHover: colorScheme.blue.blue200, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successLight: colorScheme.green.green100, - successLightHover: colorScheme.green.green200, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningLight: colorScheme.orange.orange100, - warningLightHover: colorScheme.orange.orange200, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorLight: colorScheme.red.red100, - errorLightHover: colorScheme.red.red200, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - errorSelect: colorScheme.red.alphaRed50010, - purpleLight: colorScheme.purple.purple100, - purpleLightHover: colorScheme.purple.purple200, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowySurfaceColorScheme buildSurfaceColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowySurfaceColorScheme( - primary: colorScheme.neutral.white, - overlay: colorScheme.neutral.alphaBlack60, - ), - Brightness.dark => AppFlowySurfaceColorScheme( - primary: colorScheme.neutral.neutral900, - overlay: colorScheme.neutral.alphaBlack60, - ), - }; - } - - AppFlowyBackgroundColorScheme buildBackgroundColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyBackgroundColorScheme( - primary: colorScheme.neutral.white, - secondary: colorScheme.neutral.neutral100, - tertiary: colorScheme.neutral.neutral200, - quaternary: colorScheme.neutral.neutral300, - ), - Brightness.dark => AppFlowyBackgroundColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral900, - tertiary: colorScheme.neutral.neutral800, - quaternary: colorScheme.neutral.neutral700, - ), - }; - } - - AppFlowyBrandColorScheme buildBrandColorScheme( - AppFlowyBaseColorScheme colorScheme, - ) { - return AppFlowyBrandColorScheme( - skyline: const Color(0xFF00B5FF), - aqua: const Color(0xFF00C8FF), - violet: const Color(0xFF9327FF), - amethyst: const Color(0xFF8427E0), - berry: const Color(0xFFE3006D), - coral: const Color(0xFFFB006D), - golden: const Color(0xFFF7931E), - amber: const Color(0xFFFFBD00), - lemon: const Color(0xFFFFCE00), - ); - } - - AppFlowyBorderRadius buildBorderRadius( - AppFlowyBaseColorScheme colorScheme, - ) { + AppFlowyBorderRadius buildBorderRadius() { return AppFlowyBorderRadius( xs: AppFlowyBorderRadiusConstant.radius100, s: AppFlowyBorderRadiusConstant.radius200, @@ -310,9 +35,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowySpacing buildSpacing( - AppFlowyBaseColorScheme colorScheme, - ) { + AppFlowySpacing buildSpacing() { return AppFlowySpacing( xs: AppFlowySpacingConstant.spacing100, s: AppFlowySpacingConstant.spacing200, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart new file mode 100644 index 0000000000..2b29371433 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart @@ -0,0 +1 @@ +export 'appflowy_default/semantic.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart deleted file mode 100644 index 60f7d1f1a4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/data/builder.dart'; -import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; -import 'package:appflowy_ui/src/theme/text_style/text_style.dart'; -import 'package:flutter/material.dart'; - -abstract class AppFlowyBaseTheme { - const AppFlowyBaseTheme(); - - AppFlowyBaseColorScheme get colorScheme; - - AppFlowyTextColorScheme get textColorScheme; - - AppFlowyBaseTextStyle get textStyle; - - AppFlowyIconColorTheme get iconColorTheme; - - AppFlowyBorderColorScheme get borderColorScheme; - - AppFlowyBackgroundColorScheme get backgroundColorScheme; - - AppFlowyFillColorScheme get fillColorScheme; - - AppFlowySurfaceColorScheme get surfaceColorScheme; - - AppFlowyBorderRadius get borderRadius; - - AppFlowySpacing get spacing; - - AppFlowyShadow get shadow; - - AppFlowyBrandColorScheme get brandColorScheme; -} - -class AppFlowyThemeData extends AppFlowyBaseTheme { - factory AppFlowyThemeData.light() { - final colorScheme = AppFlowyBaseColorScheme(); - - final textStyle = AppFlowyBaseTextStyle(); - final textColorScheme = themeBuilder.buildTextColorScheme( - colorScheme, - Brightness.light, - ); - final borderColorScheme = themeBuilder.buildBorderColorScheme( - colorScheme, - Brightness.light, - ); - final fillColorScheme = themeBuilder.buildFillColorScheme( - colorScheme, - Brightness.light, - ); - final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( - colorScheme, - Brightness.light, - ); - final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( - colorScheme, - Brightness.light, - ); - final iconColorTheme = themeBuilder.buildIconColorTheme( - colorScheme, - Brightness.light, - ); - final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); - final borderRadius = themeBuilder.buildBorderRadius(colorScheme); - final spacing = themeBuilder.buildSpacing(colorScheme); - final shadow = themeBuilder.buildShadow(Brightness.light); - - return AppFlowyThemeData( - colorScheme: colorScheme, - textColorScheme: textColorScheme, - textStyle: textStyle, - iconColorTheme: iconColorTheme, - backgroundColorScheme: backgroundColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - brandColorScheme: brandColorScheme, - ); - } - - factory AppFlowyThemeData.dark() { - final colorScheme = AppFlowyBaseColorScheme(); - final textStyle = AppFlowyBaseTextStyle(); - final textColorScheme = themeBuilder.buildTextColorScheme( - colorScheme, - Brightness.dark, - ); - final borderColorScheme = themeBuilder.buildBorderColorScheme( - colorScheme, - Brightness.dark, - ); - final fillColorScheme = themeBuilder.buildFillColorScheme( - colorScheme, - Brightness.dark, - ); - final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( - colorScheme, - Brightness.dark, - ); - final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( - colorScheme, - Brightness.dark, - ); - final iconColorTheme = themeBuilder.buildIconColorTheme( - colorScheme, - Brightness.dark, - ); - final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); - final borderRadius = themeBuilder.buildBorderRadius(colorScheme); - final spacing = themeBuilder.buildSpacing(colorScheme); - final shadow = themeBuilder.buildShadow(Brightness.dark); - - return AppFlowyThemeData( - colorScheme: colorScheme, - textColorScheme: textColorScheme, - textStyle: textStyle, - iconColorTheme: iconColorTheme, - backgroundColorScheme: backgroundColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - brandColorScheme: brandColorScheme, - ); - } - - const AppFlowyThemeData({ - required this.colorScheme, - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorTheme, - required this.backgroundColorScheme, - this.brightness = Brightness.light, - }); - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - final Brightness brightness; - - @override - final AppFlowyBaseColorScheme colorScheme; - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorTheme iconColorTheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - static AppFlowyTextColorScheme buildTextColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - inverse: colorScheme.neutral.white, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red600, - errorHover: colorScheme.red.red700, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - inverse: colorScheme.neutral.neutral1000, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red500, - errorHover: colorScheme.red.red400, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart new file mode 100644 index 0000000000..ca06c8da1b --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart @@ -0,0 +1,33 @@ +import 'border_radius/border_radius.dart'; +import 'color_scheme/color_scheme.dart'; +import 'shadow/shadow.dart'; +import 'spacing/spacing.dart'; +import 'text_style/text_style.dart'; + +abstract class AppFlowyBaseThemeData { + const AppFlowyBaseThemeData(); + + AppFlowyTextColorScheme get textColorScheme; + + AppFlowyBaseTextStyle get textStyle; + + AppFlowyIconColorScheme get iconColorScheme; + + AppFlowyBorderColorScheme get borderColorScheme; + + AppFlowyBackgroundColorScheme get backgroundColorScheme; + + AppFlowyFillColorScheme get fillColorScheme; + + AppFlowySurfaceColorScheme get surfaceColorScheme; + + AppFlowyBorderRadius get borderRadius; + + AppFlowySpacing get spacing; + + AppFlowyShadow get shadow; + + AppFlowyBrandColorScheme get brandColorScheme; + + AppFlowyOtherColorsColorScheme get otherColorsColorScheme; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart new file mode 100644 index 0000000000..01952e1461 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart @@ -0,0 +1,8 @@ +export 'background_color_scheme.dart'; +export 'border_color_scheme.dart'; +export 'brand_color_scheme.dart'; +export 'fill_color_scheme.dart'; +export 'icon_color_scheme.dart'; +export 'other_color_scheme.dart'; +export 'surface_color_scheme.dart'; +export 'text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart similarity index 86% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart index f9ece5339c..245a02aadb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class AppFlowyIconColorTheme { - const AppFlowyIconColorTheme({ +class AppFlowyIconColorScheme { + const AppFlowyIconColorScheme({ required this.primary, required this.secondary, required this.tertiary, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart new file mode 100644 index 0000000000..ed9b94695c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart @@ -0,0 +1,9 @@ +import 'dart:ui'; + +class AppFlowyOtherColorsColorScheme { + const AppFlowyOtherColorsColorScheme({ + required this.textHighlight, + }); + + final Color textHighlight; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart similarity index 88% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart index 1caaa288e8..d96ca0f557 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart'; +import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; class AppFlowyBaseTextStyle { const AppFlowyBaseTextStyle({ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart deleted file mode 100644 index e502b3b875..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowySpacingConstant { - static const double spacing100 = 4; - static const double spacing200 = 6; - static const double spacing300 = 8; - static const double spacing400 = 12; - static const double spacing500 = 16; - static const double spacing600 = 20; -} - -class AppFlowyBorderRadiusConstant { - static const double radius100 = 4; - static const double radius200 = 6; - static const double radius300 = 8; - static const double radius400 = 12; - static const double radius500 = 16; - static const double radius600 = 20; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart index b800b1bb6c..5f9f66cd2d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart @@ -1,7 +1,8 @@ export 'appflowy_theme.dart'; -export 'border_radius/border_radius.dart'; -export 'color_scheme/color_scheme.dart'; -export 'data/data.dart'; -export 'dimensions.dart'; -export 'spacing/spacing.dart'; -export 'text_style/text_style.dart'; +export 'data/built_in_themes.dart'; +export 'definition/border_radius/border_radius.dart'; +export 'definition/color_scheme/color_scheme.dart'; +export 'definition/base_theme.dart'; +export 'definition/spacing/spacing.dart'; +export 'definition/shadow/shadow.dart'; +export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json new file mode 100644 index 0000000000..c46354b599 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json @@ -0,0 +1,984 @@ +{ + "Neutral": { + "100": { + "$type": "color", + "$value": "#f8faff" + }, + "200": { + "$type": "color", + "$value": "#e4e8f5" + }, + "300": { + "$type": "color", + "$value": "#ced3e6" + }, + "400": { + "$type": "color", + "$value": "#b5bbd3" + }, + "500": { + "$type": "color", + "$value": "#989eb7" + }, + "600": { + "$type": "color", + "$value": "#6f748c" + }, + "700": { + "$type": "color", + "$value": "#54596e" + }, + "800": { + "$type": "color", + "$value": "#3c3f4e" + }, + "900": { + "$type": "color", + "$value": "#272930" + }, + "1000": { + "$type": "color", + "$value": "#21232a" + }, + "black": { + "$type": "color", + "$value": "#000000" + }, + "alpha-black-60": { + "$type": "color", + "$value": "#00000099" + }, + "white": { + "$type": "color", + "$value": "#ffffff" + }, + "alpha-white-0": { + "$type": "color", + "$value": "#ffffff00" + }, + "alpha-white-20": { + "$type": "color", + "$value": "#ffffff33" + }, + "alpha-white-30": { + "$type": "color", + "$value": "#ffffff4d" + }, + "alpha-grey-100-05": { + "$type": "color", + "$value": "#f9fafd0d" + }, + "alpha-grey-100-10": { + "$type": "color", + "$value": "#f9fafd1a" + }, + "alpha-grey-1000-05": { + "$type": "color", + "$value": "#1f23290d" + }, + "alpha-grey-1000-10": { + "$type": "color", + "$value": "#1f23291a" + }, + "alpha-grey-1000-70": { + "$type": "color", + "$value": "#1f2329b2" + }, + "alpha-grey-1000-80": { + "$type": "color", + "$value": "#1f2329cc" + } + }, + "Blue": { + "100": { + "$type": "color", + "$value": "#e3f6ff" + }, + "200": { + "$type": "color", + "$value": "#a9e2ff" + }, + "300": { + "$type": "color", + "$value": "#80d2ff" + }, + "400": { + "$type": "color", + "$value": "#4ec1ff" + }, + "500": { + "$type": "color", + "$value": "#00b5ff" + }, + "600": { + "$type": "color", + "$value": "#0092d6" + }, + "700": { + "$type": "color", + "$value": "#0078c0" + }, + "800": { + "$type": "color", + "$value": "#0065a9" + }, + "900": { + "$type": "color", + "$value": "#00508f" + }, + "1000": { + "$type": "color", + "$value": "#003c77" + }, + "alpha-blue-500-15": { + "$type": "color", + "$value": "#00b5ff26" + } + }, + "Green": { + "100": { + "$type": "color", + "$value": "#ecf9f5" + }, + "200": { + "$type": "color", + "$value": "#c3e5d8" + }, + "300": { + "$type": "color", + "$value": "#9ad1bc" + }, + "400": { + "$type": "color", + "$value": "#71bd9f" + }, + "500": { + "$type": "color", + "$value": "#48a982" + }, + "600": { + "$type": "color", + "$value": "#248569" + }, + "700": { + "$type": "color", + "$value": "#29725d" + }, + "800": { + "$type": "color", + "$value": "#2e6050" + }, + "900": { + "$type": "color", + "$value": "#305548" + }, + "1000": { + "$type": "color", + "$value": "#305244" + } + }, + "Purple": { + "100": { + "$type": "color", + "$value": "#f1e0ff" + }, + "200": { + "$type": "color", + "$value": "#e1b3ff" + }, + "300": { + "$type": "color", + "$value": "#d185ff" + }, + "400": { + "$type": "color", + "$value": "#bc58ff" + }, + "500": { + "$type": "color", + "$value": "#9327ff" + }, + "600": { + "$type": "color", + "$value": "#7a1dcc" + }, + "700": { + "$type": "color", + "$value": "#6617b3" + }, + "800": { + "$type": "color", + "$value": "#55138f" + }, + "900": { + "$type": "color", + "$value": "#470c72" + }, + "1000": { + "$type": "color", + "$value": "#380758" + } + }, + "Magenta": { + "100": { + "$type": "color", + "$value": "#ffe5ef" + }, + "200": { + "$type": "color", + "$value": "#ffb8d1" + }, + "300": { + "$type": "color", + "$value": "#ff8ab2" + }, + "400": { + "$type": "color", + "$value": "#ff5c93" + }, + "500": { + "$type": "color", + "$value": "#fb006d" + }, + "600": { + "$type": "color", + "$value": "#d2005f" + }, + "700": { + "$type": "color", + "$value": "#d2005f" + }, + "800": { + "$type": "color", + "$value": "#850040" + }, + "900": { + "$type": "color", + "$value": "#610031" + }, + "1000": { + "$type": "color", + "$value": "#400022" + } + }, + "Red": { + "100": { + "$type": "color", + "$value": "#ffd2dd" + }, + "200": { + "$type": "color", + "$value": "#ffa5b4" + }, + "300": { + "$type": "color", + "$value": "#ff7d87" + }, + "400": { + "$type": "color", + "$value": "#ff5050" + }, + "500": { + "$type": "color", + "$value": "#f33641" + }, + "600": { + "$type": "color", + "$value": "#e71d32" + }, + "700": { + "$type": "color", + "$value": "#ad1625" + }, + "800": { + "$type": "color", + "$value": "#8c101c" + }, + "900": { + "$type": "color", + "$value": "#6e0a1e" + }, + "1000": { + "$type": "color", + "$value": "#4c0a17" + }, + "alpha-red-500-10": { + "$type": "color", + "$value": "#f336411a" + } + }, + "Orange": { + "100": { + "$type": "color", + "$value": "#fff3d5" + }, + "200": { + "$type": "color", + "$value": "#ffe4ab" + }, + "300": { + "$type": "color", + "$value": "#ffd181" + }, + "400": { + "$type": "color", + "$value": "#ffbe62" + }, + "500": { + "$type": "color", + "$value": "#ffa02e" + }, + "600": { + "$type": "color", + "$value": "#db7e21" + }, + "700": { + "$type": "color", + "$value": "#b75f17" + }, + "800": { + "$type": "color", + "$value": "#93450e" + }, + "900": { + "$type": "color", + "$value": "#7a3108" + }, + "1000": { + "$type": "color", + "$value": "#602706" + } + }, + "Yellow": { + "100": { + "$type": "color", + "$value": "#fff9b2" + }, + "200": { + "$type": "color", + "$value": "#ffec66" + }, + "300": { + "$type": "color", + "$value": "#ffdf1a" + }, + "400": { + "$type": "color", + "$value": "#ffcc00" + }, + "500": { + "$type": "color", + "$value": "#ffce00" + }, + "600": { + "$type": "color", + "$value": "#e6b800" + }, + "700": { + "$type": "color", + "$value": "#cc9f00" + }, + "800": { + "$type": "color", + "$value": "#b38a00" + }, + "900": { + "$type": "color", + "$value": "#9a7500" + }, + "1000": { + "$type": "color", + "$value": "#7f6200" + } + }, + "Subtle_Color": { + "Rose": { + "100": { + "$type": "color", + "$value": "#fcf2f2" + }, + "200": { + "$type": "color", + "$value": "#fae3e3" + }, + "300": { + "$type": "color", + "$value": "#fad9d9" + }, + "400": { + "$type": "color", + "$value": "#edadad" + }, + "500": { + "$type": "color", + "$value": "#cc4e4e" + }, + "600": { + "$type": "color", + "$value": "#702828" + } + }, + "Papaya": { + "100": { + "$type": "color", + "$value": "#fcf4f0" + }, + "200": { + "$type": "color", + "$value": "#fae8de" + }, + "300": { + "$type": "color", + "$value": "#fadfd2" + }, + "400": { + "$type": "color", + "$value": "#f0bda3" + }, + "500": { + "$type": "color", + "$value": "#d67240" + }, + "600": { + "$type": "color", + "$value": "#6b3215" + } + }, + "Tangerine": { + "100": { + "$type": "color", + "$value": "#fff7ed" + }, + "200": { + "$type": "color", + "$value": "#fcedd9" + }, + "300": { + "$type": "color", + "$value": "#fae5ca" + }, + "400": { + "$type": "color", + "$value": "#f2cb99" + }, + "500": { + "$type": "color", + "$value": "#db8f2c" + }, + "600": { + "$type": "color", + "$value": "#613b0a" + } + }, + "Mango": { + "100": { + "$type": "color", + "$value": "#fff9ec" + }, + "200": { + "$type": "color", + "$value": "#fcf1d7" + }, + "300": { + "$type": "color", + "$value": "#fae9c3" + }, + "400": { + "$type": "color", + "$value": "#f5d68e" + }, + "500": { + "$type": "color", + "$value": "#e0a416" + }, + "600": { + "$type": "color", + "$value": "#5c4102" + } + }, + "Lemon": { + "100": { + "$type": "color", + "$value": "#fffbe8" + }, + "200": { + "$type": "color", + "$value": "#fcf5cf" + }, + "300": { + "$type": "color", + "$value": "#faefb9" + }, + "400": { + "$type": "color", + "$value": "#f5e282" + }, + "500": { + "$type": "color", + "$value": "#e0bb00" + }, + "600": { + "$type": "color", + "$value": "#574800" + } + }, + "Olive": { + "100": { + "$type": "color", + "$value": "#f9fae6" + }, + "200": { + "$type": "color", + "$value": "#f6f7d0" + }, + "300": { + "$type": "color", + "$value": "#f0f2b3" + }, + "400": { + "$type": "color", + "$value": "#dbde83" + }, + "500": { + "$type": "color", + "$value": "#adb204" + }, + "600": { + "$type": "color", + "$value": "#4a4c03" + } + }, + "Lime": { + "100": { + "$type": "color", + "$value": "#f6f9e6" + }, + "200": { + "$type": "color", + "$value": "#eef5ce" + }, + "300": { + "$type": "color", + "$value": "#e7f0bb" + }, + "400": { + "$type": "color", + "$value": "#cfdb91" + }, + "500": { + "$type": "color", + "$value": "#92a822" + }, + "600": { + "$type": "color", + "$value": "#414d05" + } + }, + "Grass": { + "100": { + "$type": "color", + "$value": "#f4faeb" + }, + "200": { + "$type": "color", + "$value": "#e9f5d7" + }, + "300": { + "$type": "color", + "$value": "#def0c5" + }, + "400": { + "$type": "color", + "$value": "#bfd998" + }, + "500": { + "$type": "color", + "$value": "#75a828" + }, + "600": { + "$type": "color", + "$value": "#334d0c" + } + }, + "Forest": { + "100": { + "$type": "color", + "$value": "#f1faf0" + }, + "200": { + "$type": "color", + "$value": "#e2f5df" + }, + "300": { + "$type": "color", + "$value": "#d7f0d3" + }, + "400": { + "$type": "color", + "$value": "#a8d6a1" + }, + "500": { + "$type": "color", + "$value": "#49a33b" + }, + "600": { + "$type": "color", + "$value": "#1e4f16" + } + }, + "Jade": { + "100": { + "$type": "color", + "$value": "#f0faf6" + }, + "200": { + "$type": "color", + "$value": "#dff5eb" + }, + "300": { + "$type": "color", + "$value": "#cef0e1" + }, + "400": { + "$type": "color", + "$value": "#90d1b5" + }, + "500": { + "$type": "color", + "$value": "#1c9963" + }, + "600": { + "$type": "color", + "$value": "#075231" + } + }, + "Aqua": { + "100": { + "$type": "color", + "$value": "#f0f9fa" + }, + "200": { + "$type": "color", + "$value": "#dff3f5" + }, + "300": { + "$type": "color", + "$value": "#ccecf0" + }, + "400": { + "$type": "color", + "$value": "#83ccd4" + }, + "500": { + "$type": "color", + "$value": "#008e9e" + }, + "600": { + "$type": "color", + "$value": "#004e57" + } + }, + "Azure": { + "100": { + "$type": "color", + "$value": "#f0f6fa" + }, + "200": { + "$type": "color", + "$value": "#e1eef7" + }, + "300": { + "$type": "color", + "$value": "#d3e6f5" + }, + "400": { + "$type": "color", + "$value": "#88c0eb" + }, + "500": { + "$type": "color", + "$value": "#0877cc" + }, + "600": { + "$type": "color", + "$value": "#154469" + } + }, + "Denim": { + "100": { + "$type": "color", + "$value": "#f0f3fa" + }, + "200": { + "$type": "color", + "$value": "#e3ebfa" + }, + "300": { + "$type": "color", + "$value": "#d7e2f7" + }, + "400": { + "$type": "color", + "$value": "#9ab6ed" + }, + "500": { + "$type": "color", + "$value": "#3267d1" + }, + "600": { + "$type": "color", + "$value": "#223c70" + } + }, + "Mauve": { + "100": { + "$type": "color", + "$value": "#f2f2fc" + }, + "200": { + "$type": "color", + "$value": "#e6e6fa" + }, + "300": { + "$type": "color", + "$value": "#dcdcf7" + }, + "400": { + "$type": "color", + "$value": "#aeaef5" + }, + "500": { + "$type": "color", + "$value": "#5555e0" + }, + "600": { + "$type": "color", + "$value": "#36366b" + } + }, + "Lavender": { + "100": { + "$type": "color", + "$value": "#f6f3fc" + }, + "200": { + "$type": "color", + "$value": "#ebe3fa" + }, + "300": { + "$type": "color", + "$value": "#e4daf7" + }, + "400": { + "$type": "color", + "$value": "#c1aaf0" + }, + "500": { + "$type": "color", + "$value": "#8153db" + }, + "600": { + "$type": "color", + "$value": "#462f75" + } + }, + "Lilac": { + "100": { + "$type": "color", + "$value": "#f7f0fa" + }, + "200": { + "$type": "color", + "$value": "#f0e1f7" + }, + "300": { + "$type": "color", + "$value": "#edd7f7" + }, + "400": { + "$type": "color", + "$value": "#d3a9e8" + }, + "500": { + "$type": "color", + "$value": "#9e4cc7" + }, + "600": { + "$type": "color", + "$value": "#562d6b" + } + }, + "Mallow": { + "100": { + "$type": "color", + "$value": "#faf0fa" + }, + "200": { + "$type": "color", + "$value": "#f5e1f4" + }, + "300": { + "$type": "color", + "$value": "#f5d7f4" + }, + "400": { + "$type": "color", + "$value": "#dea4dc" + }, + "500": { + "$type": "color", + "$value": "#b240af" + }, + "600": { + "$type": "color", + "$value": "#632861" + } + }, + "Camellia": { + "100": { + "$type": "color", + "$value": "#f9eff3" + }, + "200": { + "$type": "color", + "$value": "#f7e1eb" + }, + "300": { + "$type": "color", + "$value": "#f7d7e5" + }, + "400": { + "$type": "color", + "$value": "#e5a3c0" + }, + "500": { + "$type": "color", + "$value": "#c24279" + }, + "600": { + "$type": "color", + "$value": "#6e2343" + } + }, + "Smoke": { + "100": { + "$type": "color", + "$value": "#f5f5f5" + }, + "200": { + "$type": "color", + "$value": "#e8e8e8" + }, + "300": { + "$type": "color", + "$value": "#dedede" + }, + "400": { + "$type": "color", + "$value": "#b8b8b8" + }, + "500": { + "$type": "color", + "$value": "#6e6e6e" + }, + "600": { + "$type": "color", + "$value": "#404040" + } + }, + "Iron": { + "100": { + "$type": "color", + "$value": "#f2f4f7" + }, + "200": { + "$type": "color", + "$value": "#e6e9f0" + }, + "300": { + "$type": "color", + "$value": "#dadee5" + }, + "400": { + "$type": "color", + "$value": "#b0b5bf" + }, + "500": { + "$type": "color", + "$value": "#666f80" + }, + "600": { + "$type": "color", + "$value": "#394152" + } + } + }, + "Spacing": { + "0": { + "$type": "dimension", + "$value": "0px" + }, + "100": { + "$type": "dimension", + "$value": "4px" + }, + "200": { + "$type": "dimension", + "$value": "6px" + }, + "300": { + "$type": "dimension", + "$value": "8px" + }, + "400": { + "$type": "dimension", + "$value": "12px" + }, + "500": { + "$type": "dimension", + "$value": "16px" + }, + "600": { + "$type": "dimension", + "$value": "20px" + }, + "1000": { + "$type": "dimension", + "$value": "1000px" + } + }, + "Border-Radius": { + "0": { + "$type": "dimension", + "$value": "0px" + }, + "100": { + "$type": "dimension", + "$value": "4px" + }, + "200": { + "$type": "dimension", + "$value": "6px" + }, + "300": { + "$type": "dimension", + "$value": "8px" + }, + "400": { + "$type": "dimension", + "$value": "12px" + }, + "500": { + "$type": "dimension", + "$value": "16px" + }, + "600": { + "$type": "dimension", + "$value": "20px" + }, + "1000": { + "$type": "dimension", + "$value": "1000px" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json new file mode 100644 index 0000000000..99d266c008 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json @@ -0,0 +1,1039 @@ +{ + "Text": { + "primary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "inverse": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "on-fill": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "theme": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "action": { + "$type": "color", + "$value": "{Blue.500}" + }, + "action-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-hover": { + "$type": "color", + "$value": "{Red.400}" + }, + "purple": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Icon": { + "primary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "purple-thick": { + "$type": "color", + "$value": "#ffffff" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "#ffffff" + } + }, + "Border": { + "grey-primary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "grey-primary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "grey-secondary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "grey-secondary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "grey-tertiary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "grey-tertiary-hover": { + "$type": "color", + "$value": "{Neutral.700}" + }, + "grey-quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "grey-quaternary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.400}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Fill": { + "primary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "primary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "secondary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary-hover": { + "$type": "color", + "$value": "{Neutral.500}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "quaternary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "primary-alpha-5": { + "$type": "color", + "$value": "{Neutral.alpha-grey-100-05}", + "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." + }, + "primary-alpha-5-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-100-10}" + }, + "primary-alpha-80": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-80}" + }, + "primary-alpha-80-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-70}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "white-alpha": { + "$type": "color", + "$value": "{Neutral.alpha-white-20}" + }, + "white-alpha-hover": { + "$type": "color", + "$value": "{Neutral.alpha-white-30}" + }, + "black": { + "$type": "color", + "$value": "{Neutral.black}" + }, + "theme-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "theme-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.400}" + }, + "theme-select": { + "$type": "color", + "$value": "{Blue.alpha-blue-500-15}" + }, + "info-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "info-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-light": { + "$type": "color", + "$value": "{Green.100}" + }, + "success-light-hover": { + "$type": "color", + "$value": "{Green.200}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-light": { + "$type": "color", + "$value": "{Orange.100}" + }, + "warning-light-hover": { + "$type": "color", + "$value": "{Orange.200}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-light": { + "$type": "color", + "$value": "{Red.100}" + }, + "error-light-hover": { + "$type": "color", + "$value": "{Red.200}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-select": { + "$type": "color", + "$value": "{Red.alpha-red-500-10}" + }, + "purple-light": { + "$type": "color", + "$value": "{Purple.100}" + }, + "purple-light-hover": { + "$type": "color", + "$value": "{Purple.200}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + } + }, + "Surface": { + "primary": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "overlay": { + "$type": "color", + "$value": "{Neutral.alpha-black-60}" + } + }, + "Background": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.700}" + } + }, + "Badge_Color": { + "Rose": { + "rose-light-1": { + "$type": "color", + "$value": "#fcf2f2" + }, + "rose-light-2": { + "$type": "color", + "$value": "#fae3e3" + }, + "rose-light-3": { + "$type": "color", + "$value": "#fad9d9" + }, + "rose-thick-1": { + "$type": "color", + "$value": "#edadad" + }, + "rose-thick-2": { + "$type": "color", + "$value": "#cc4e4e" + }, + "rose-thick-3": { + "$type": "color", + "$value": "#702828" + } + }, + "Papaya": { + "papaya-light-1": { + "$type": "color", + "$value": "#fcf4f0" + }, + "papaya-light-2": { + "$type": "color", + "$value": "#fae8de" + }, + "papaya-light-3": { + "$type": "color", + "$value": "#fadfd2" + }, + "papaya-thick-1": { + "$type": "color", + "$value": "#f0bda3" + }, + "papaya-thick-2": { + "$type": "color", + "$value": "#d67240" + }, + "papaya-thick-3": { + "$type": "color", + "$value": "#6b3215" + } + }, + "Tangerine": { + "tangerine-light-1": { + "$type": "color", + "$value": "#fff7ed" + }, + "tangerine-light-2": { + "$type": "color", + "$value": "#fcedd9" + }, + "tangerine-light-3": { + "$type": "color", + "$value": "#fae5ca" + }, + "tangerine-thick-1": { + "$type": "color", + "$value": "#f2cb99" + }, + "tangerine-thick-2": { + "$type": "color", + "$value": "#db8f2c" + }, + "tangerine-thick-3": { + "$type": "color", + "$value": "#613b0a" + } + }, + "Mango": { + "mango-light-1": { + "$type": "color", + "$value": "#fff9ec" + }, + "mango-light-2": { + "$type": "color", + "$value": "#fcf1d7" + }, + "mango-light-3": { + "$type": "color", + "$value": "#fae9c3" + }, + "mango-thick-1": { + "$type": "color", + "$value": "#f5d68e" + }, + "mango-thick-2": { + "$type": "color", + "$value": "#e0a416" + }, + "mango-thick-3": { + "$type": "color", + "$value": "#5c4102" + } + }, + "Lemon": { + "lemon-light-1": { + "$type": "color", + "$value": "#fffbe8" + }, + "lemon-light-2": { + "$type": "color", + "$value": "#fcf5cf" + }, + "lemon-light-3": { + "$type": "color", + "$value": "#faefb9" + }, + "lemon-thick-1": { + "$type": "color", + "$value": "#f5e282" + }, + "lemon-thick-2": { + "$type": "color", + "$value": "#e0bb00" + }, + "lemon-thick-3": { + "$type": "color", + "$value": "#574800" + } + }, + "Olive": { + "olive-light-1": { + "$type": "color", + "$value": "#f9fae6" + }, + "olive-light-2": { + "$type": "color", + "$value": "#f6f7d0" + }, + "olive-light-3": { + "$type": "color", + "$value": "#f0f2b3" + }, + "olive-thick-1": { + "$type": "color", + "$value": "#dbde83" + }, + "olive-thick-2": { + "$type": "color", + "$value": "#adb204" + }, + "olive-thick-3": { + "$type": "color", + "$value": "#4a4c03" + } + }, + "Lime": { + "lime-light-1": { + "$type": "color", + "$value": "#f6f9e6" + }, + "lime-light-2": { + "$type": "color", + "$value": "#eef5ce" + }, + "lime-light-3": { + "$type": "color", + "$value": "#e7f0bb" + }, + "lime-thick-1": { + "$type": "color", + "$value": "#cfdb91" + }, + "lime-thick-2": { + "$type": "color", + "$value": "#92a822" + }, + "lime-thick-3": { + "$type": "color", + "$value": "#414d05" + } + }, + "Grass": { + "grass-light-1": { + "$type": "color", + "$value": "#f4faeb" + }, + "grass-light-2": { + "$type": "color", + "$value": "#e9f5d7" + }, + "grass-light-3": { + "$type": "color", + "$value": "#def0c5" + }, + "grass-thick-1": { + "$type": "color", + "$value": "#bfd998" + }, + "grass-thick-2": { + "$type": "color", + "$value": "#75a828" + }, + "grass-thick-3": { + "$type": "color", + "$value": "#334d0c" + } + }, + "Forest": { + "forest-light-1": { + "$type": "color", + "$value": "#f1faf0" + }, + "forest-light-2": { + "$type": "color", + "$value": "#e2f5df" + }, + "forest-light-3": { + "$type": "color", + "$value": "#d7f0d3" + }, + "forest-thick-1": { + "$type": "color", + "$value": "#a8d6a1" + }, + "forest-thick-2": { + "$type": "color", + "$value": "#49a33b" + }, + "forest-thick-3": { + "$type": "color", + "$value": "#1e4f16" + } + }, + "Jade": { + "jade-light-1": { + "$type": "color", + "$value": "#f0faf6" + }, + "jade-light-2": { + "$type": "color", + "$value": "#dff5eb" + }, + "jade-light-3": { + "$type": "color", + "$value": "#cef0e1" + }, + "jade-thick-1": { + "$type": "color", + "$value": "#90d1b5" + }, + "jade-thick-2": { + "$type": "color", + "$value": "#1c9963" + }, + "jade-thick-3": { + "$type": "color", + "$value": "#075231" + } + }, + "Aqua": { + "aqua-light-1": { + "$type": "color", + "$value": "#f0f9fa" + }, + "aqua-light-2": { + "$type": "color", + "$value": "#dff3f5" + }, + "aqua-light-3": { + "$type": "color", + "$value": "#ccecf0" + }, + "aqua-thick-1": { + "$type": "color", + "$value": "#83ccd4" + }, + "aqua-thick-2": { + "$type": "color", + "$value": "#008e9e" + }, + "aqua-thick-3": { + "$type": "color", + "$value": "#004e57" + } + }, + "Azure": { + "azure-light-1": { + "$type": "color", + "$value": "#f0f6fa" + }, + "azure-light-2": { + "$type": "color", + "$value": "#e1eef7" + }, + "azure-light-3": { + "$type": "color", + "$value": "#d3e6f5" + }, + "azure-thick-1": { + "$type": "color", + "$value": "#88c0eb" + }, + "azure-thick-2": { + "$type": "color", + "$value": "#0877cc" + }, + "azure-thick-3": { + "$type": "color", + "$value": "#154469" + } + }, + "Denim": { + "denim-light-1": { + "$type": "color", + "$value": "#f0f3fa" + }, + "denim-light-2": { + "$type": "color", + "$value": "#e3ebfa" + }, + "denim-light-3": { + "$type": "color", + "$value": "#d7e2f7" + }, + "denim-thick-1": { + "$type": "color", + "$value": "#9ab6ed" + }, + "denim-thick-2": { + "$type": "color", + "$value": "#3267d1" + }, + "denim-thick-3": { + "$type": "color", + "$value": "#223c70" + } + }, + "Mauve": { + "mauve-light-1": { + "$type": "color", + "$value": "#f2f2fc" + }, + "mauve-thick-2": { + "$type": "color", + "$value": "#5555e0" + }, + "mauve-thick-3": { + "$type": "color", + "$value": "#36366b" + }, + "mauve-thick-1": { + "$type": "color", + "$value": "#aeaef5" + } + }, + "Lavender": { + "lavender-light-1": { + "$type": "color", + "$value": "#f6f3fc" + }, + "lavender-light-2": { + "$type": "color", + "$value": "#ebe3fa" + }, + "lavender-light-3": { + "$type": "color", + "$value": "#e4daf7" + }, + "lavender-thick-1": { + "$type": "color", + "$value": "#c1aaf0" + }, + "lavender-thick-2": { + "$type": "color", + "$value": "#8153db" + }, + "lavender-thick-3": { + "$type": "color", + "$value": "#462f75" + } + }, + "Lilac": { + "liliac-light-1": { + "$type": "color", + "$value": "#f7f0fa" + }, + "liliac-light-2": { + "$type": "color", + "$value": "#f0e1f7" + }, + "liliac-light-3": { + "$type": "color", + "$value": "#edd7f7" + }, + "liliac-thick-1": { + "$type": "color", + "$value": "#d3a9e8" + }, + "liliac-thick-2": { + "$type": "color", + "$value": "#9e4cc7" + }, + "liliac-thick-3": { + "$type": "color", + "$value": "#562d6b" + } + }, + "Mallow": { + "mallow-light-1": { + "$type": "color", + "$value": "#faf0fa" + }, + "mallow-light-2": { + "$type": "color", + "$value": "#f5e1f4" + }, + "mallow-light-3": { + "$type": "color", + "$value": "#f5d7f4" + }, + "mallow-thick-1": { + "$type": "color", + "$value": "#dea4dc" + }, + "mallow-thick-2": { + "$type": "color", + "$value": "#b240af" + }, + "mallow-thick-3": { + "$type": "color", + "$value": "#632861" + } + }, + "Camellia": { + "camellia-light-1": { + "$type": "color", + "$value": "#f9eff3" + }, + "camellia-light-2": { + "$type": "color", + "$value": "#f7e1eb" + }, + "camellia-light-3": { + "$type": "color", + "$value": "#f7d7e5" + }, + "camellia-thick-1": { + "$type": "color", + "$value": "#e5a3c0" + }, + "camellia-thick-2": { + "$type": "color", + "$value": "#c24279" + }, + "camellia-thick-3": { + "$type": "color", + "$value": "#6e2343" + } + }, + "Smoke": { + "smoke-light-1": { + "$type": "color", + "$value": "#f5f5f5" + }, + "smoke-light-2": { + "$type": "color", + "$value": "#e8e8e8" + }, + "smoke-light-3": { + "$type": "color", + "$value": "#dedede" + }, + "smoke-thick-1": { + "$type": "color", + "$value": "#b8b8b8" + }, + "smoke-thick-2": { + "$type": "color", + "$value": "#6e6e6e" + }, + "smoke-thick-3": { + "$type": "color", + "$value": "#404040" + } + }, + "Iron": { + "icon-light-1": { + "$type": "color", + "$value": "#f2f4f7" + }, + "icon-light-2": { + "$type": "color", + "$value": "#e6e9f0" + }, + "icon-light-3": { + "$type": "color", + "$value": "#dadee5" + }, + "icon-thick-1": { + "$type": "color", + "$value": "#b0b5bf" + }, + "icon-thick-2": { + "$type": "color", + "$value": "#666f80" + }, + "icon-thick-3": { + "$type": "color", + "$value": "#394152" + } + } + }, + "Shadow": { + "sm": { + "$type": "dimension", + "$value": "0px" + }, + "md": { + "$type": "dimension", + "$value": "0px" + } + }, + "Brand": { + "Skyline": { + "$type": "color", + "$value": "#00b5ff" + }, + "Aqua": { + "$type": "color", + "$value": "#00c8ff" + }, + "Violet": { + "$type": "color", + "$value": "#9327ff" + }, + "Amethyst": { + "$type": "color", + "$value": "#8427e0" + }, + "Berry": { + "$type": "color", + "$value": "#e3006d" + }, + "Coral": { + "$type": "color", + "$value": "#fb006d" + }, + "Golden": { + "$type": "color", + "$value": "#f7931e" + }, + "Amber": { + "$type": "color", + "$value": "#ffbd00" + }, + "Lemon": { + "$type": "color", + "$value": "#ffce00" + } + }, + "Other_Colors": { + "text-highlight": { + "$type": "color", + "$value": "{Blue.200}" + } + }, + "Spacing": { + "spacing-0": { + "$type": "dimension", + "$value": "{Spacing.0}" + }, + "spacing-xs": { + "$type": "dimension", + "$value": "{Spacing.100}" + }, + "spacing-s": { + "$type": "dimension", + "$value": "{Spacing.200}" + }, + "spacing-m": { + "$type": "dimension", + "$value": "{Spacing.300}" + }, + "spacing-l": { + "$type": "dimension", + "$value": "{Spacing.400}" + }, + "spacing-xl": { + "$type": "dimension", + "$value": "{Spacing.500}" + }, + "spacing-xxl": { + "$type": "dimension", + "$value": "{Spacing.600}" + }, + "spacing-full": { + "$type": "dimension", + "$value": "{Spacing.1000}" + } + }, + "Border_Radius": { + "border-radius-0": { + "$type": "dimension", + "$value": "{Border-Radius.0}" + }, + "border-radius-xs": { + "$type": "dimension", + "$value": "{Border-Radius.100}" + }, + "border-radius-s": { + "$type": "dimension", + "$value": "{Border-Radius.200}" + }, + "border-radius-m": { + "$type": "dimension", + "$value": "{Border-Radius.300}" + }, + "border-radius-l": { + "$type": "dimension", + "$value": "{Border-Radius.400}" + }, + "border-radius-xl": { + "$type": "dimension", + "$value": "{Border-Radius.500}" + }, + "border-radius-xxl": { + "$type": "dimension", + "$value": "{Border-Radius.600}" + }, + "border-radius-full": { + "$type": "dimension", + "$value": "{Border-Radius.1000}" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json new file mode 100644 index 0000000000..4e6b0543dc --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json @@ -0,0 +1,1039 @@ +{ + "Text": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "inverse": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "on-fill": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "theme": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "action": { + "$type": "color", + "$value": "{Blue.500}" + }, + "action-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "purple": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Icon": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Border": { + "grey-primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "grey-primary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "grey-secondary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "grey-secondary-hover": { + "$type": "color", + "$value": "{Neutral.700}" + }, + "grey-tertiary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "grey-tertiary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "grey-quaternary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "grey-quaternary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Fill": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "primary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "secondary-hover": { + "$type": "color", + "$value": "{Neutral.500}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "tertiary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "quaternary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "primary-alpha-5": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-05}", + "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." + }, + "primary-alpha-5-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-10}" + }, + "primary-alpha-80": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-80}" + }, + "primary-alpha-80-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-70}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "white-alpha": { + "$type": "color", + "$value": "{Neutral.alpha-white-20}" + }, + "white-alpha-hover": { + "$type": "color", + "$value": "{Neutral.alpha-white-30}" + }, + "black": { + "$type": "color", + "$value": "{Neutral.black}" + }, + "theme-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "theme-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "theme-select": { + "$type": "color", + "$value": "{Blue.alpha-blue-500-15}" + }, + "info-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "info-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-light": { + "$type": "color", + "$value": "{Green.100}" + }, + "success-light-hover": { + "$type": "color", + "$value": "{Green.200}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-light": { + "$type": "color", + "$value": "{Orange.100}" + }, + "warning-light-hover": { + "$type": "color", + "$value": "{Orange.200}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-light": { + "$type": "color", + "$value": "{Red.100}" + }, + "error-light-hover": { + "$type": "color", + "$value": "{Red.200}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "error-select": { + "$type": "color", + "$value": "{Red.alpha-red-500-10}" + }, + "purple-light": { + "$type": "color", + "$value": "{Purple.100}" + }, + "purple-light-hover": { + "$type": "color", + "$value": "{Purple.200}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + } + }, + "Surface": { + "primary": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "overlay": { + "$type": "color", + "$value": "{Neutral.alpha-black-60}" + } + }, + "Background": { + "primary": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.300}" + } + }, + "Badge_Color": { + "Rose": { + "rose-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Rose.100}" + }, + "rose-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Rose.200}" + }, + "rose-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Rose.300}" + }, + "rose-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Rose.400}" + }, + "rose-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Rose.500}" + }, + "rose-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Rose.600}" + } + }, + "Papaya": { + "papaya-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.100}" + }, + "papaya-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.200}" + }, + "papaya-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.300}" + }, + "papaya-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.400}" + }, + "papaya-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.500}" + }, + "papaya-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.600}" + } + }, + "Tangerine": { + "tangerine-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.100}" + }, + "tangerine-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.200}" + }, + "tangerine-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.300}" + }, + "tangerine-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.400}" + }, + "tangerine-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.500}" + }, + "tangerine-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.600}" + } + }, + "Mango": { + "mango-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mango.100}" + }, + "mango-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Mango.200}" + }, + "mango-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Mango.300}" + }, + "mango-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mango.400}" + }, + "mango-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mango.500}" + }, + "mango-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mango.600}" + } + }, + "Lemon": { + "lemon-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.100}" + }, + "lemon-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.200}" + }, + "lemon-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.300}" + }, + "lemon-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.400}" + }, + "lemon-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.500}" + }, + "lemon-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.600}" + } + }, + "Olive": { + "olive-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Olive.100}" + }, + "olive-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Olive.200}" + }, + "olive-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Olive.300}" + }, + "olive-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Olive.400}" + }, + "olive-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Olive.500}" + }, + "olive-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Olive.600}" + } + }, + "Lime": { + "lime-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lime.100}" + }, + "lime-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lime.200}" + }, + "lime-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lime.300}" + }, + "lime-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lime.400}" + }, + "lime-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lime.500}" + }, + "lime-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lime.600}" + } + }, + "Grass": { + "grass-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Grass.100}" + }, + "grass-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Grass.200}" + }, + "grass-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Grass.300}" + }, + "grass-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Grass.400}" + }, + "grass-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Grass.500}" + }, + "grass-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Grass.600}" + } + }, + "Forest": { + "forest-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Forest.100}" + }, + "forest-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Forest.200}" + }, + "forest-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Forest.300}" + }, + "forest-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Forest.400}" + }, + "forest-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Forest.500}" + }, + "forest-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Forest.600}" + } + }, + "Jade": { + "jade-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Jade.100}" + }, + "jade-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Jade.200}" + }, + "jade-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Jade.300}" + }, + "jade-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Jade.400}" + }, + "jade-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Jade.500}" + }, + "jade-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Jade.600}" + } + }, + "Aqua": { + "aqua-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.100}" + }, + "aqua-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.200}" + }, + "aqua-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.300}" + }, + "aqua-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.400}" + }, + "aqua-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.500}" + }, + "aqua-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.600}" + } + }, + "Azure": { + "azure-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Azure.100}" + }, + "azure-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Azure.200}" + }, + "azure-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Azure.300}" + }, + "azure-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Azure.400}" + }, + "azure-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Azure.500}" + }, + "azure-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Azure.600}" + } + }, + "Denim": { + "denim-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Denim.100}" + }, + "denim-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Denim.200}" + }, + "denim-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Denim.300}" + }, + "denim-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Denim.400}" + }, + "denim-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Denim.500}" + }, + "denim-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Denim.600}" + } + }, + "Mauve": { + "mauve-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.100}" + }, + "mauve-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.500}" + }, + "mauve-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.600}" + }, + "mauve-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.400}" + } + }, + "Lavender": { + "lavender-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.100}" + }, + "lavender-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.200}" + }, + "lavender-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.300}" + }, + "lavender-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.400}" + }, + "lavender-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.500}" + }, + "lavender-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.600}" + } + }, + "Lilac": { + "liliac-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.100}" + }, + "liliac-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.200}" + }, + "liliac-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.300}" + }, + "liliac-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.400}" + }, + "liliac-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.500}" + }, + "liliac-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.600}" + } + }, + "Mallow": { + "mallow-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.100}" + }, + "mallow-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.200}" + }, + "mallow-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.300}" + }, + "mallow-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.400}" + }, + "mallow-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.500}" + }, + "mallow-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.600}" + } + }, + "Camellia": { + "camellia-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.100}" + }, + "camellia-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.200}" + }, + "camellia-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.300}" + }, + "camellia-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.400}" + }, + "camellia-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.500}" + }, + "camellia-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.600}" + } + }, + "Smoke": { + "smoke-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.100}" + }, + "smoke-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.200}" + }, + "smoke-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.300}" + }, + "smoke-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.400}" + }, + "smoke-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.500}" + }, + "smoke-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.600}" + } + }, + "Iron": { + "icon-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Iron.100}" + }, + "icon-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Iron.200}" + }, + "icon-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Iron.300}" + }, + "icon-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Iron.400}" + }, + "icon-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Iron.500}" + }, + "icon-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Iron.600}" + } + } + }, + "Shadow": { + "sm": { + "$type": "dimension", + "$value": "0px" + }, + "md": { + "$type": "dimension", + "$value": "0px" + } + }, + "Brand": { + "Skyline": { + "$type": "color", + "$value": "#00b5ff" + }, + "Aqua": { + "$type": "color", + "$value": "#00c8ff" + }, + "Violet": { + "$type": "color", + "$value": "#9327ff" + }, + "Amethyst": { + "$type": "color", + "$value": "#8427e0" + }, + "Berry": { + "$type": "color", + "$value": "#e3006d" + }, + "Coral": { + "$type": "color", + "$value": "#fb006d" + }, + "Golden": { + "$type": "color", + "$value": "#f7931e" + }, + "Amber": { + "$type": "color", + "$value": "#ffbd00" + }, + "Lemon": { + "$type": "color", + "$value": "#ffce00" + } + }, + "Other_Colors": { + "text-highlight": { + "$type": "color", + "$value": "{Blue.200}" + } + }, + "Spacing": { + "spacing-0": { + "$type": "dimension", + "$value": "{Spacing.0}" + }, + "spacing-xs": { + "$type": "dimension", + "$value": "{Spacing.100}" + }, + "spacing-s": { + "$type": "dimension", + "$value": "{Spacing.200}" + }, + "spacing-m": { + "$type": "dimension", + "$value": "{Spacing.300}" + }, + "spacing-l": { + "$type": "dimension", + "$value": "{Spacing.400}" + }, + "spacing-xl": { + "$type": "dimension", + "$value": "{Spacing.500}" + }, + "spacing-xxl": { + "$type": "dimension", + "$value": "{Spacing.600}" + }, + "spacing-full": { + "$type": "dimension", + "$value": "{Spacing.1000}" + } + }, + "Border_Radius": { + "border-radius-0": { + "$type": "dimension", + "$value": "{Border-Radius.0}" + }, + "border-radius-xs": { + "$type": "dimension", + "$value": "{Border-Radius.100}" + }, + "border-radius-s": { + "$type": "dimension", + "$value": "{Border-Radius.200}" + }, + "border-radius-m": { + "$type": "dimension", + "$value": "{Border-Radius.300}" + }, + "border-radius-l": { + "$type": "dimension", + "$value": "{Border-Radius.400}" + }, + "border-radius-xl": { + "$type": "dimension", + "$value": "{Border-Radius.500}" + }, + "border-radius-xxl": { + "$type": "dimension", + "$value": "{Border-Radius.600}" + }, + "border-radius-full": { + "$type": "dimension", + "$value": "{Border-Radius.1000}" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart new file mode 100644 index 0000000000..9c9c8a45ff --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -0,0 +1,269 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; + +void main() { + generatePrimitive(); + generateSemantic(); +} + +void generatePrimitive() { + // 1. Load the JSON file. + final jsonString = + File('script/Primitive.Mode 1.tokens.json').readAsStringSync(); + final jsonData = jsonDecode(jsonString) as Map; + + // 2. Prepare the output code. + final buffer = StringBuffer(); + + buffer.writeln(''' +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: ${DateTime.now().toIso8601String()} +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:flutter/material.dart'; + +class AppFlowyPrimitiveTokens { + AppFlowyPrimitiveTokens._();'''); + + // 3. Process each color category. + jsonData.forEach((categoryName, categoryData) { + if (categoryData is Map) { + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map && + tokenData['\$type'] == 'color') { + final colorValue = tokenData['\$value'] as String; + final dartColorValue = convertColor(colorValue); + final dartTokenName = + '${categoryName}_$tokenName'.replaceAll('-', '_').toCamelCase(); + + buffer.writeln(''' + + /// $colorValue + static Color get $dartTokenName => Color(0x$dartColorValue);'''); + } + }); + } + }); + + buffer.writeln('}'); + + // 4. Write the output to a Dart file. + final outputFile = File('lib/src/theme/data/appflowy_default/primitive.dart'); + outputFile.writeAsStringSync(buffer.toString()); + + print('Successfully generated ${outputFile.path}'); +} + +void generateSemantic() { + // 1. Load the JSON file. + final lightJsonString = + File('script/Semantic.Light Mode.tokens.json').readAsStringSync(); + final darkJsonString = + File('script/Semantic.Dark Mode.tokens.json').readAsStringSync(); + final lightJsonData = jsonDecode(lightJsonString) as Map; + final darkJsonData = jsonDecode(darkJsonString) as Map; + + // 2. Prepare the output code. + final buffer = StringBuffer(); + + buffer.writeln(''' +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: ${DateTime.now().toIso8601String()} +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +import '../builder.dart'; +import 'primitive.dart'; + +class AppFlowyThemeData implements AppFlowyBaseThemeData { + const AppFlowyThemeData._({ + required this.textStyle, + required this.textColorScheme, + required this.borderColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.iconColorScheme, + required this.backgroundColorScheme, + required this.otherColorsColorScheme, + }); +'''); + + // 3. Process light mode semantic tokens + void writeThemeFactory(String brightness, Map jsonData) { + buffer.writeln(''' + factory AppFlowyThemeData.$brightness() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.$brightness);'''); + + jsonData.forEach((categoryName, categoryData) { + if (categoryData is Map) { + final hasNonColorType = categoryData.values.any( + (element) => + element is Map && element['\$type'] != 'color', + ); + if (hasNonColorType) { + return; + } + + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); + + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map && + tokenData['\$type'] == 'color') { + final semanticTokenName = + tokenName.replaceAll('-', '_').toCamelCase(); + + final value = tokenData['\$value'] as String; + final String colorOrPrimitiveToken; + if (value.isColor) { + colorOrPrimitiveToken = 'Color(0x${convertColor(value)})'; + } else { + final primitiveToken = value + .replaceAll('{', '') + .replaceAll('}', '') + .replaceAll('.', '_') + .replaceAll('-', '_') + .toCamelCase(); + colorOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; + } + + buffer.writeln(' $semanticTokenName: $colorOrPrimitiveToken,'); + } + }); + buffer.writeln(' );'); + } + }); + + buffer.writeln(); + buffer.writeln(''' + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + }'''); + } + + writeThemeFactory('light', lightJsonData); + buffer.writeln(); + writeThemeFactory('dark', darkJsonData); + + buffer.writeln(''' + + static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); + + @override + final AppFlowyBaseTextStyle textStyle; + + @override + final AppFlowyTextColorScheme textColorScheme; + + @override + final AppFlowyBorderColorScheme borderColorScheme; + + @override + final AppFlowyFillColorScheme fillColorScheme; + + @override + final AppFlowySurfaceColorScheme surfaceColorScheme; + + @override + final AppFlowyBorderRadius borderRadius; + + @override + final AppFlowySpacing spacing; + + @override + final AppFlowyShadow shadow; + + @override + final AppFlowyBrandColorScheme brandColorScheme; + + @override + final AppFlowyIconColorScheme iconColorScheme; + + @override + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + @override + final AppFlowyOtherColorsColorScheme otherColorsColorScheme;'''); + buffer.writeln('}'); + + // 4. Write the output to a Dart file. + final outputFile = File('lib/src/theme/data/appflowy_default/semantic.dart'); + outputFile.writeAsStringSync(buffer.toString()); + + print('Successfully generated ${outputFile.path}'); +} + +String convertColor(String hexColor) { + String color = hexColor.toUpperCase().replaceAll('#', ''); + if (color.length == 6) { + color = 'FF$color'; // Add missing alpha channel + } else if (color.length == 8) { + color = color.substring(6) + color.substring(0, 6); // Rearrange to ARGB + } + return color; +} + +extension on String { + String toCamelCase() { + return split('_').mapIndexed((index, part) { + if (index == 0) { + return part.toLowerCase(); + } else { + return part[0].toUpperCase() + part.substring(1).toLowerCase(); + } + }).join(); + } + + String toCapitalize() { + if (isEmpty) { + return this; + } + return '${this[0].toUpperCase()}${substring(1)}'; + } + + bool get isColor => + startsWith('#') || + (startsWith('0x') && length == 10) || + (startsWith('0xFF') && length == 12); +} From 28e89beb432605d01c46db9829c866c056bf32a7 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:14:38 +0800 Subject: [PATCH 363/384] feat: appflowy theme lerp (#7771) * feat: implement appflowy theme lerping * refactor: use theme builder and adjust script * chore: rename theme data * chore: add doc comments * chore: rename function * chore: don't use theme extension * chore: use the animated appflowy theme widget * chore: clean up inherited theme --- .../lib/startup/tasks/app_widget.dart | 47 ++++---- .../appearance/mobile_appearance.dart | 17 +-- .../appflowy_ui/example/lib/main.dart | 12 +- .../src/component/textfield/textfield.dart | 4 +- .../lib/src/theme/appflowy_theme.dart | 105 ++++++++++++++++-- .../data/appflowy_default/primitive.dart | 2 +- .../theme/data/appflowy_default/semantic.dart | 81 +++----------- .../src/theme/data/custom/custom_theme.dart | 23 ++++ .../theme/data/{builder.dart => shared.dart} | 10 +- .../lib/src/theme/definition/base_theme.dart | 33 ------ .../color_scheme/background_color_scheme.dart | 12 ++ .../color_scheme/border_color_scheme.dart | 36 ++++++ .../color_scheme/brand_color_scheme.dart | 17 +++ .../color_scheme/fill_color_scheme.dart | 59 ++++++++++ .../color_scheme/icon_color_scheme.dart | 16 +++ .../color_scheme/other_color_scheme.dart | 9 ++ .../color_scheme/surface_color_scheme.dart | 10 ++ .../color_scheme/text_color_scheme.dart | 28 +++++ .../lib/src/theme/definition/theme_data.dart | 86 ++++++++++++++ .../appflowy_ui/lib/src/theme/theme.dart | 2 +- .../appflowy_ui/script/generate_theme.dart | 78 +++---------- 21 files changed, 467 insertions(+), 220 deletions(-) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/{builder.dart => shared.dart} (92%) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 0ef21267aa..98b76802d4 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -234,27 +234,34 @@ class _ApplicationWidgetState extends State { supportedLocales: context.supportedLocales, locale: state.locale, routerConfig: routerConfig, - builder: (context, child) => AppFlowyTheme( - data: Theme.of(context).brightness == Brightness.light - ? AppFlowyThemeData.light() - : AppFlowyThemeData.dark(), - child: MediaQuery( - // use the 1.0 as the textScaleFactor to avoid the text size - // affected by the system setting. - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(state.textScaleFactor), + builder: (context, child) { + final themeBuilder = AppFlowyDefaultTheme(); + final brightness = Theme.of(context).brightness; + + return AnimatedAppFlowyTheme( + data: brightness == Brightness.light + ? themeBuilder.light() + : themeBuilder.dark(), + child: MediaQuery( + // use the 1.0 as the textScaleFactor to avoid the text size + // affected by the system setting. + data: MediaQuery.of(context).copyWith( + textScaler: + TextScaler.linear(state.textScaleFactor), + ), + child: overlayManagerBuilder( + context, + !UniversalPlatform.isMobile && + FeatureFlag.search.isOn + ? CommandPalette( + notifier: _commandPaletteNotifier, + child: child, + ) + : child, + ), ), - child: overlayManagerBuilder( - context, - !UniversalPlatform.isMobile && FeatureFlag.search.isOn - ? CommandPalette( - notifier: _commandPaletteNotifier, - child: child, - ) - : child, - ), - ), - ), + ); + }, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index eda3153459..46eddd53ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -28,13 +28,12 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, ); + final isLight = brightness == Brightness.light; final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - final theme = brightness == Brightness.light - ? appTheme.lightTheme - : appTheme.darkTheme; + final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; - final colorTheme = brightness == Brightness.light + final colorTheme = isLight ? ColorScheme( brightness: brightness, primary: _primaryColor, @@ -71,13 +70,9 @@ class MobileAppearance extends BaseAppearance { onSurface: const Color(0xffC5C6C7), // text/body color surfaceContainerHighest: theme.sidebarBg, ); - final hintColor = brightness == Brightness.light - ? const Color(0x991F2329) - : _hintColorInDarkMode; - final onBackground = - brightness == Brightness.light ? _onBackgroundColor : Colors.white; - final background = - brightness == Brightness.light ? Colors.white : const Color(0xff121212); + final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode; + final onBackground = isLight ? _onBackgroundColor : Colors.white; + final background = isLight ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index c02001745d..0d23746ebd 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -26,16 +26,20 @@ class MyApp extends StatelessWidget { return ValueListenableBuilder( valueListenable: themeMode, builder: (context, themeMode, child) { + final themeBuilder = AppFlowyDefaultTheme(); final themeData = themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); - return AppFlowyTheme( + + return AnimatedAppFlowyTheme( data: themeMode == ThemeMode.light - ? AppFlowyThemeData.light() - : AppFlowyThemeData.dark(), + ? themeBuilder.light() + : themeBuilder.dark(), child: MaterialApp( debugShowCheckedModeBanner: false, title: 'AppFlowy UI Example', - theme: themeData.copyWith(visualDensity: VisualDensity.standard), + theme: themeData.copyWith( + visualDensity: VisualDensity.standard, + ), home: const MyHomePage( title: 'AppFlowy UI', ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 9e61b71709..3f5ad4cfed 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -233,7 +233,7 @@ enum AFTextFieldSize { m, l; - EdgeInsetsGeometry contentPadding(AppFlowyBaseThemeData theme) { + EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { return EdgeInsets.symmetric( vertical: switch (this) { AFTextFieldSize.m => theme.spacing.s, @@ -243,7 +243,7 @@ enum AFTextFieldSize { ); } - BorderRadius borderRadius(AppFlowyBaseThemeData theme) { + BorderRadius borderRadius(AppFlowyThemeData theme) { return BorderRadius.circular( switch (this) { AFTextFieldSize.m => theme.borderRadius.m, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart index 8a99b737ec..26e45ca8f1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -1,5 +1,6 @@ -import 'package:appflowy_ui/src/theme/definition/base_theme.dart'; -import 'package:flutter/widgets.dart'; +import 'package:appflowy_ui/src/theme/theme.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; class AppFlowyTheme extends StatelessWidget { const AppFlowyTheme({ @@ -8,10 +9,10 @@ class AppFlowyTheme extends StatelessWidget { required this.child, }); - final AppFlowyBaseThemeData data; + final AppFlowyThemeData data; final Widget child; - static AppFlowyBaseThemeData of(BuildContext context, {bool listen = true}) { + static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { final provider = maybeOf(context, listen: listen); if (provider == null) { throw FlutterError( @@ -26,26 +27,26 @@ class AppFlowyTheme extends StatelessWidget { return provider; } - static AppFlowyBaseThemeData? maybeOf( + static AppFlowyThemeData? maybeOf( BuildContext context, { bool listen = true, }) { if (listen) { return context .dependOnInheritedWidgetOfExactType() - ?.theme; + ?.themeData; } final provider = context .getElementForInheritedWidgetOfExactType() ?.widget; - return (provider as AppFlowyInheritedTheme?)?.theme; + return (provider as AppFlowyInheritedTheme?)?.themeData; } @override Widget build(BuildContext context) { return AppFlowyInheritedTheme( - theme: data, + themeData: data, child: child, ); } @@ -54,18 +55,98 @@ class AppFlowyTheme extends StatelessWidget { class AppFlowyInheritedTheme extends InheritedTheme { const AppFlowyInheritedTheme({ super.key, - required this.theme, + required this.themeData, required super.child, }); - final AppFlowyBaseThemeData theme; + final AppFlowyThemeData themeData; @override Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: theme, child: child); + return AppFlowyTheme(data: themeData, child: child); } @override bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - theme != oldWidget.theme; + themeData != oldWidget.themeData; +} + +/// An interpolation between two [AppFlowyThemeData]s. +/// +/// This class specializes the interpolation of [Tween] to +/// call the [AppFlowyThemeData.lerp] method. +/// +/// See [Tween] for a discussion on how to use interpolation objects. +class AppFlowyThemeDataTween extends Tween { + /// Creates a [AppFlowyThemeData] tween. + /// + /// The [begin] and [end] properties must be non-null before the tween is + /// first used, but the arguments can be null if the values are going to be + /// filled in later. + AppFlowyThemeDataTween({super.begin, super.end}); + + @override + AppFlowyThemeData lerp(double t) => AppFlowyThemeData.lerp(begin!, end!, t); +} + +class AnimatedAppFlowyTheme extends ImplicitlyAnimatedWidget { + /// Creates an animated theme. + /// + /// By default, the theme transition uses a linear curve. + const AnimatedAppFlowyTheme({ + super.key, + required this.data, + super.curve, + super.duration = kThemeAnimationDuration, + super.onEnd, + required this.child, + }); + + /// Specifies the color and typography values for descendant widgets. + final AppFlowyThemeData data; + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + + @override + AnimatedWidgetBaseState createState() => + _AnimatedThemeState(); +} + +class _AnimatedThemeState + extends AnimatedWidgetBaseState { + AppFlowyThemeDataTween? data; + + @override + void forEachTween(TweenVisitor visitor) { + data = visitor( + data, + widget.data, + (dynamic value) => + AppFlowyThemeDataTween(begin: value as AppFlowyThemeData), + )! as AppFlowyThemeDataTween; + } + + @override + Widget build(BuildContext context) { + return AppFlowyTheme( + data: data!.evaluate(animation), + child: widget.child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add( + DiagnosticsProperty( + 'data', + data, + showName: false, + defaultValue: null, + ), + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart index 1eee06953f..790db31660 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-15T23:05:39.278903 +// Generation time: 2025-04-16T22:13:33.297893 // // To modify these colors, edit the source JSON files and run the script: // diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index 08206a6572..03e80da962 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-15T23:05:39.288085 +// Generation time: 2025-04-16T22:13:33.307397 // // To modify these colors, edit the source JSON files and run the script: // @@ -12,30 +12,16 @@ import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; -import '../builder.dart'; +import '../shared.dart'; import 'primitive.dart'; -class AppFlowyThemeData implements AppFlowyBaseThemeData { - const AppFlowyThemeData._({ - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorScheme, - required this.backgroundColorScheme, - required this.otherColorsColorScheme, - }); - - factory AppFlowyThemeData.light() { +class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { + @override + AppFlowyThemeData light() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.light); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.light); final textColorScheme = AppFlowyTextColorScheme( primary: AppFlowyPrimitiveTokens.neutral1000, @@ -168,7 +154,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { textHighlight: AppFlowyPrimitiveTokens.blue200, ); - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -184,11 +170,12 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { ); } - factory AppFlowyThemeData.dark() { + @override + AppFlowyThemeData dark() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.dark); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark); final textColorScheme = AppFlowyTextColorScheme( primary: AppFlowyPrimitiveTokens.neutral200, @@ -321,7 +308,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { textHighlight: AppFlowyPrimitiveTokens.blue200, ); - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -336,42 +323,4 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { shadow: shadow, ); } - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorScheme iconColorScheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - @override - final AppFlowyOtherColorsColorScheme otherColorsColorScheme; } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart new file mode 100644 index 0000000000..6ef43076c5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart @@ -0,0 +1,23 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; + +class CustomTheme implements AppFlowyThemeBuilder { + const CustomTheme({ + required this.lightThemeJson, + required this.darkThemeJson, + }); + + final Map lightThemeJson; + final Map darkThemeJson; + + @override + AppFlowyThemeData light() { + // TODO: implement light + throw UnimplementedError(); + } + + @override + AppFlowyThemeData dark() { + // TODO: implement dark + throw UnimplementedError(); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart similarity index 92% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart index a4f83109cb..c9c3c3adb0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart @@ -21,10 +21,10 @@ class AppFlowyBorderRadiusConstant { static const double radius600 = 20; } -class AppFlowyThemeBuilder { - const AppFlowyThemeBuilder(); +class AppFlowySharedTokens { + const AppFlowySharedTokens(); - AppFlowyBorderRadius buildBorderRadius() { + static AppFlowyBorderRadius buildBorderRadius() { return AppFlowyBorderRadius( xs: AppFlowyBorderRadiusConstant.radius100, s: AppFlowyBorderRadiusConstant.radius200, @@ -35,7 +35,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowySpacing buildSpacing() { + static AppFlowySpacing buildSpacing() { return AppFlowySpacing( xs: AppFlowySpacingConstant.spacing100, s: AppFlowySpacingConstant.spacing200, @@ -46,7 +46,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowyShadow buildShadow( + static AppFlowyShadow buildShadow( Brightness brightness, ) { return switch (brightness) { diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart deleted file mode 100644 index ca06c8da1b..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'border_radius/border_radius.dart'; -import 'color_scheme/color_scheme.dart'; -import 'shadow/shadow.dart'; -import 'spacing/spacing.dart'; -import 'text_style/text_style.dart'; - -abstract class AppFlowyBaseThemeData { - const AppFlowyBaseThemeData(); - - AppFlowyTextColorScheme get textColorScheme; - - AppFlowyBaseTextStyle get textStyle; - - AppFlowyIconColorScheme get iconColorScheme; - - AppFlowyBorderColorScheme get borderColorScheme; - - AppFlowyBackgroundColorScheme get backgroundColorScheme; - - AppFlowyFillColorScheme get fillColorScheme; - - AppFlowySurfaceColorScheme get surfaceColorScheme; - - AppFlowyBorderRadius get borderRadius; - - AppFlowySpacing get spacing; - - AppFlowyShadow get shadow; - - AppFlowyBrandColorScheme get brandColorScheme; - - AppFlowyOtherColorsColorScheme get otherColorsColorScheme; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart index 547c7f1635..c7324c34fe 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart @@ -12,4 +12,16 @@ class AppFlowyBackgroundColorScheme { final Color secondary; final Color tertiary; final Color quaternary; + + AppFlowyBackgroundColorScheme lerp( + AppFlowyBackgroundColorScheme other, + double t, + ) { + return AppFlowyBackgroundColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart index d1618b6cff..28eee5b145 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart @@ -46,4 +46,40 @@ class AppFlowyBorderColorScheme { final Color errorThickHover; final Color purpleThick; final Color purpleThickHover; + + AppFlowyBorderColorScheme lerp( + AppFlowyBorderColorScheme other, + double t, + ) { + return AppFlowyBorderColorScheme( + greyPrimary: Color.lerp(greyPrimary, other.greyPrimary, t)!, + greyPrimaryHover: + Color.lerp(greyPrimaryHover, other.greyPrimaryHover, t)!, + greySecondary: Color.lerp(greySecondary, other.greySecondary, t)!, + greySecondaryHover: + Color.lerp(greySecondaryHover, other.greySecondaryHover, t)!, + greyTertiary: Color.lerp(greyTertiary, other.greyTertiary, t)!, + greyTertiaryHover: + Color.lerp(greyTertiaryHover, other.greyTertiaryHover, t)!, + greyQuaternary: Color.lerp(greyQuaternary, other.greyQuaternary, t)!, + greyQuaternaryHover: + Color.lerp(greyQuaternaryHover, other.greyQuaternaryHover, t)!, + transparent: Color.lerp(transparent, other.transparent, t)!, + themeThick: Color.lerp(themeThick, other.themeThick, t)!, + themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, + infoThick: Color.lerp(infoThick, other.infoThick, t)!, + infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, + successThick: Color.lerp(successThick, other.successThick, t)!, + successThickHover: + Color.lerp(successThickHover, other.successThickHover, t)!, + warningThick: Color.lerp(warningThick, other.warningThick, t)!, + warningThickHover: + Color.lerp(warningThickHover, other.warningThickHover, t)!, + errorThick: Color.lerp(errorThick, other.errorThick, t)!, + errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart index 8374d2bbfd..4140f6924a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart @@ -22,4 +22,21 @@ class AppFlowyBrandColorScheme { final Color golden; final Color amber; final Color lemon; + + AppFlowyBrandColorScheme lerp( + AppFlowyBrandColorScheme other, + double t, + ) { + return AppFlowyBrandColorScheme( + skyline: Color.lerp(skyline, other.skyline, t)!, + aqua: Color.lerp(aqua, other.aqua, t)!, + violet: Color.lerp(violet, other.violet, t)!, + amethyst: Color.lerp(amethyst, other.amethyst, t)!, + berry: Color.lerp(berry, other.berry, t)!, + coral: Color.lerp(coral, other.coral, t)!, + golden: Color.lerp(golden, other.golden, t)!, + amber: Color.lerp(amber, other.amber, t)!, + lemon: Color.lerp(lemon, other.lemon, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart index 8616bde4eb..3faac64dfc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart @@ -90,4 +90,63 @@ class AppFlowyFillColorScheme { final Color purpleLightHover; final Color purpleThick; final Color purpleThickHover; + + AppFlowyFillColorScheme lerp( + AppFlowyFillColorScheme other, + double t, + ) { + return AppFlowyFillColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + tertiaryHover: Color.lerp(tertiaryHover, other.tertiaryHover, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + quaternaryHover: Color.lerp(quaternaryHover, other.quaternaryHover, t)!, + transparent: Color.lerp(transparent, other.transparent, t)!, + primaryAlpha5: Color.lerp(primaryAlpha5, other.primaryAlpha5, t)!, + primaryAlpha5Hover: + Color.lerp(primaryAlpha5Hover, other.primaryAlpha5Hover, t)!, + primaryAlpha80: Color.lerp(primaryAlpha80, other.primaryAlpha80, t)!, + primaryAlpha80Hover: + Color.lerp(primaryAlpha80Hover, other.primaryAlpha80Hover, t)!, + white: Color.lerp(white, other.white, t)!, + whiteAlpha: Color.lerp(whiteAlpha, other.whiteAlpha, t)!, + whiteAlphaHover: Color.lerp(whiteAlphaHover, other.whiteAlphaHover, t)!, + black: Color.lerp(black, other.black, t)!, + themeLight: Color.lerp(themeLight, other.themeLight, t)!, + themeLightHover: Color.lerp(themeLightHover, other.themeLightHover, t)!, + themeThick: Color.lerp(themeThick, other.themeThick, t)!, + themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, + themeSelect: Color.lerp(themeSelect, other.themeSelect, t)!, + infoLight: Color.lerp(infoLight, other.infoLight, t)!, + infoLightHover: Color.lerp(infoLightHover, other.infoLightHover, t)!, + infoThick: Color.lerp(infoThick, other.infoThick, t)!, + infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, + successLight: Color.lerp(successLight, other.successLight, t)!, + successLightHover: + Color.lerp(successLightHover, other.successLightHover, t)!, + successThick: Color.lerp(successThick, other.successThick, t)!, + successThickHover: + Color.lerp(successThickHover, other.successThickHover, t)!, + warningLight: Color.lerp(warningLight, other.warningLight, t)!, + warningLightHover: + Color.lerp(warningLightHover, other.warningLightHover, t)!, + warningThick: Color.lerp(warningThick, other.warningThick, t)!, + warningThickHover: + Color.lerp(warningThickHover, other.warningThickHover, t)!, + errorLight: Color.lerp(errorLight, other.errorLight, t)!, + errorLightHover: Color.lerp(errorLightHover, other.errorLightHover, t)!, + errorThick: Color.lerp(errorThick, other.errorThick, t)!, + errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, + errorSelect: Color.lerp(errorSelect, other.errorSelect, t)!, + purpleLight: Color.lerp(purpleLight, other.purpleLight, t)!, + purpleLightHover: + Color.lerp(purpleLightHover, other.purpleLightHover, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart index 245a02aadb..efe59b8b99 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart @@ -18,4 +18,20 @@ class AppFlowyIconColorScheme { final Color white; final Color purpleThick; final Color purpleThickHover; + + AppFlowyIconColorScheme lerp( + AppFlowyIconColorScheme other, + double t, + ) { + return AppFlowyIconColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + white: Color.lerp(white, other.white, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart index ed9b94695c..9bb21e54e6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart @@ -6,4 +6,13 @@ class AppFlowyOtherColorsColorScheme { }); final Color textHighlight; + + AppFlowyOtherColorsColorScheme lerp( + AppFlowyOtherColorsColorScheme other, + double t, + ) { + return AppFlowyOtherColorsColorScheme( + textHighlight: Color.lerp(textHighlight, other.textHighlight, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart index 8fdc21adef..67be450a04 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart @@ -8,4 +8,14 @@ class AppFlowySurfaceColorScheme { final Color primary; final Color overlay; + + AppFlowySurfaceColorScheme lerp( + AppFlowySurfaceColorScheme other, + double t, + ) { + return AppFlowySurfaceColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + overlay: Color.lerp(overlay, other.overlay, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart index 486378643f..17e1f057ce 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart @@ -44,4 +44,32 @@ class AppFlowyTextColorScheme { final Color errorHover; final Color purple; final Color purpleHover; + + AppFlowyTextColorScheme lerp( + AppFlowyTextColorScheme other, + double t, + ) { + return AppFlowyTextColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + inverse: Color.lerp(inverse, other.inverse, t)!, + onFill: Color.lerp(onFill, other.onFill, t)!, + theme: Color.lerp(theme, other.theme, t)!, + themeHover: Color.lerp(themeHover, other.themeHover, t)!, + action: Color.lerp(action, other.action, t)!, + actionHover: Color.lerp(actionHover, other.actionHover, t)!, + info: Color.lerp(info, other.info, t)!, + infoHover: Color.lerp(infoHover, other.infoHover, t)!, + success: Color.lerp(success, other.success, t)!, + successHover: Color.lerp(successHover, other.successHover, t)!, + warning: Color.lerp(warning, other.warning, t)!, + warningHover: Color.lerp(warningHover, other.warningHover, t)!, + error: Color.lerp(error, other.error, t)!, + errorHover: Color.lerp(errorHover, other.errorHover, t)!, + purple: Color.lerp(purple, other.purple, t)!, + purpleHover: Color.lerp(purpleHover, other.purpleHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart new file mode 100644 index 0000000000..515e6b2ecf --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart @@ -0,0 +1,86 @@ +import 'border_radius/border_radius.dart'; +import 'color_scheme/color_scheme.dart'; +import 'shadow/shadow.dart'; +import 'spacing/spacing.dart'; +import 'text_style/text_style.dart'; + +/// [AppFlowyThemeData] defines the structure of the design system, and contains +/// the data that all child widgets will have access to. +class AppFlowyThemeData { + const AppFlowyThemeData({ + required this.textColorScheme, + required this.textStyle, + required this.iconColorScheme, + required this.borderColorScheme, + required this.backgroundColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.otherColorsColorScheme, + }); + + final AppFlowyTextColorScheme textColorScheme; + + final AppFlowyBaseTextStyle textStyle; + + final AppFlowyIconColorScheme iconColorScheme; + + final AppFlowyBorderColorScheme borderColorScheme; + + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + final AppFlowyFillColorScheme fillColorScheme; + + final AppFlowySurfaceColorScheme surfaceColorScheme; + + final AppFlowyBorderRadius borderRadius; + + final AppFlowySpacing spacing; + + final AppFlowyShadow shadow; + + final AppFlowyBrandColorScheme brandColorScheme; + + final AppFlowyOtherColorsColorScheme otherColorsColorScheme; + + static AppFlowyThemeData lerp( + AppFlowyThemeData begin, + AppFlowyThemeData end, + double t, + ) { + return AppFlowyThemeData( + textColorScheme: begin.textColorScheme.lerp(end.textColorScheme, t), + textStyle: end.textStyle, + iconColorScheme: begin.iconColorScheme.lerp(end.iconColorScheme, t), + borderColorScheme: begin.borderColorScheme.lerp(end.borderColorScheme, t), + backgroundColorScheme: + begin.backgroundColorScheme.lerp(end.backgroundColorScheme, t), + fillColorScheme: begin.fillColorScheme.lerp(end.fillColorScheme, t), + surfaceColorScheme: + begin.surfaceColorScheme.lerp(end.surfaceColorScheme, t), + borderRadius: end.borderRadius, + spacing: end.spacing, + shadow: end.shadow, + brandColorScheme: begin.brandColorScheme.lerp(end.brandColorScheme, t), + otherColorsColorScheme: + begin.otherColorsColorScheme.lerp(end.otherColorsColorScheme, t), + ); + } +} + +/// [AppFlowyThemeBuilder] is used to build the light and dark themes. Extend +/// this class to create a built-in theme, or use the [CustomTheme] class to +/// create a custom theme from JSON data. +/// +/// See also: +/// +/// - [AppFlowyThemeData] for the main theme data class. +abstract class AppFlowyThemeBuilder { + const AppFlowyThemeBuilder(); + + AppFlowyThemeData light(); + AppFlowyThemeData dark(); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart index 5f9f66cd2d..000b7a0372 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart @@ -2,7 +2,7 @@ export 'appflowy_theme.dart'; export 'data/built_in_themes.dart'; export 'definition/border_radius/border_radius.dart'; export 'definition/color_scheme/color_scheme.dart'; -export 'definition/base_theme.dart'; +export 'definition/theme_data.dart'; export 'definition/spacing/spacing.dart'; export 'definition/shadow/shadow.dart'; export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart index 9c9c8a45ff..9d429f537e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print, depend_on_referenced_packages + import 'dart:convert'; import 'dart:io'; @@ -90,34 +92,20 @@ void generateSemantic() { import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; -import '../builder.dart'; +import '../shared.dart'; import 'primitive.dart'; -class AppFlowyThemeData implements AppFlowyBaseThemeData { - const AppFlowyThemeData._({ - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorScheme, - required this.backgroundColorScheme, - required this.otherColorsColorScheme, - }); -'''); +class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); // 3. Process light mode semantic tokens - void writeThemeFactory(String brightness, Map jsonData) { + void writeThemeData(String brightness, Map jsonData) { buffer.writeln(''' - factory AppFlowyThemeData.$brightness() { + @override + AppFlowyThemeData $brightness() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.$brightness);'''); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.$brightness);'''); jsonData.forEach((categoryName, categoryData) { if (categoryData is Map) { @@ -165,7 +153,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { buffer.writeln(); buffer.writeln(''' - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -182,49 +170,9 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { }'''); } - writeThemeFactory('light', lightJsonData); + writeThemeData('light', lightJsonData); buffer.writeln(); - writeThemeFactory('dark', darkJsonData); - - buffer.writeln(''' - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorScheme iconColorScheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - @override - final AppFlowyOtherColorsColorScheme otherColorsColorScheme;'''); + writeThemeData('dark', darkJsonData); buffer.writeln('}'); // 4. Write the output to a Dart file. From edc5710e323c2985c5b46ca8f7a7060b2a9cac9e Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:00:51 +0800 Subject: [PATCH 364/384] chore: auth type and remove unused code --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +-- .../user_profile/user_profile_bloc.dart | 13 +- .../bottom_sheet/bottom_sheet_view_page.dart | 2 +- .../favorite/mobile_favorite_page.dart | 8 +- .../presentation/home/mobile_home_page.dart | 14 +- .../home/setting/settings_popup_menu.dart | 2 +- .../home/tab/mobile_space_tab.dart | 3 +- .../mobile_notifications_page.dart | 10 +- .../setting/user_session_setting_group.dart | 2 +- .../application/sync/database_sync_bloc.dart | 6 +- .../database/widgets/row/row_banner.dart | 2 +- .../document/application/document_bloc.dart | 2 +- .../document_collaborators_bloc.dart | 2 +- .../application/document_sync_bloc.dart | 2 +- .../editor_plugins/file/file_util.dart | 8 +- .../page_style/_page_style_cover_image.dart | 2 +- .../lib/plugins/shared/share/share_bloc.dart | 2 +- .../icon_emoji_picker/icon_uploader.dart | 2 +- .../lib/startup/tasks/generate_router.dart | 18 -- .../user/application/encrypt_secret_bloc.dart | 114 -------- .../application/password/password_bloc.dart | 2 +- .../lib/user/application/user_listener.dart | 27 +- .../lib/user/application/user_service.dart | 10 - .../lib/user/presentation/anon_user.dart | 5 +- .../helpers/handle_user_profile_result.dart | 6 +- .../lib/user/presentation/router.dart | 14 - .../screens/encrypt_secret_screen.dart | 130 --------- .../user/presentation/screens/screens.dart | 1 - .../sign_in_screen/sign_in_screen.dart | 7 +- .../presentation/screens/splash_screen.dart | 34 +-- .../workspace/application/home/home_bloc.dart | 17 +- .../application/home/home_setting_bloc.dart | 10 +- .../settings/ai/settings_ai_bloc.dart | 12 +- .../settings/settings_dialog_bloc.dart | 2 +- .../application/user/user_workspace_bloc.dart | 2 +- .../home/desktop_home_screen.dart | 16 +- .../home/menu/sidebar/sidebar.dart | 2 +- .../menu/view/view_more_action_button.dart | 2 +- .../pages/account/account_sign_in_out.dart | 4 +- .../settings/pages/settings_account_view.dart | 31 +- .../pages/settings_workspace_view.dart | 4 +- .../settings/settings_dialog.dart | 2 +- .../widgets/setting_third_party_login.dart | 9 +- .../settings/widgets/settings_menu.dart | 6 +- .../more_view_actions/more_view_actions.dart | 2 +- .../bloc_test/home_test/view_bloc_test.dart | 19 +- .../event-integration-test/src/lib.rs | 27 +- .../event-integration-test/src/user_event.rs | 4 +- .../tests/chat/chat_message_test.rs | 14 +- .../af_cloud_test/file_upload_test.rs | 1 + .../user/af_cloud_test/anon_user_test.rs | 2 +- .../tests/user/af_cloud_test/auth_test.rs | 27 -- .../user/af_cloud_test/workspace_test.rs | 16 +- .../user/local_test/user_profile_test.rs | 29 +- .../rust-lib/flowy-ai/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 10 +- .../rust-lib/flowy-core/src/server_layer.rs | 72 +++-- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 1 + .../flowy-error/src/impl_from/database.rs | 6 +- .../flowy-folder/src/entities/workspace.rs | 2 +- .../flowy-folder/src/event_handler.rs | 2 +- .../rust-lib/flowy-folder/src/event_map.rs | 2 +- frontend/rust-lib/flowy-folder/src/manager.rs | 8 +- .../flowy-server/src/af_cloud/define.rs | 25 +- .../src/af_cloud/impls/database.rs | 16 +- .../src/af_cloud/impls/document.rs | 6 +- .../flowy-server/src/af_cloud/impls/folder.rs | 12 +- .../af_cloud/impls/user/cloud_service_impl.rs | 67 +++-- .../src/af_cloud/impls/user/dto.rs | 30 +- .../flowy-server/src/af_cloud/impls/util.rs | 7 +- .../flowy-server/src/af_cloud/server.rs | 65 ++--- .../src/local_server/impls/chat.rs | 4 +- .../src/local_server/impls/user.rs | 10 +- .../flowy-server/src/local_server/server.rs | 6 +- .../flowy-server/tests/af_cloud_test/util.rs | 8 +- frontend/rust-lib/flowy-sqlite/src/schema.rs | 37 +-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 22 +- .../rust-lib/flowy-user-pub/src/entities.rs | 84 +----- .../flowy-user/src/entities/user_profile.rs | 62 +--- .../flowy-user/src/entities/workspace.rs | 48 +++- .../rust-lib/flowy-user/src/event_handler.rs | 145 +++------- frontend/rust-lib/flowy-user/src/event_map.rs | 14 +- .../rust-lib/flowy-user/src/notification.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 17 +- .../flowy-user/src/services/sqlite_sql/mod.rs | 1 + .../src/services/sqlite_sql/user_sql.rs | 41 +-- .../src/services/sqlite_sql/workspace_sql.rs | 129 +++++---- .../flowy-user/src/user_manager/manager.rs | 176 +++++------- .../src/user_manager/manager_history_user.rs | 2 +- .../user_manager/manager_user_awareness.rs | 4 +- .../user_manager/manager_user_encryption.rs | 35 --- .../user_manager/manager_user_workspace.rs | 270 +++++++++--------- 93 files changed, 802 insertions(+), 1407 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 4b7ed5d639..92e52a1a79 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 + saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart index 1480cc02e9..0527316860 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart @@ -19,14 +19,13 @@ class UserProfileBloc extends Bloc { Future _initialize(Emitter emit) async { emit(const UserProfileState.loading()); - - final workspaceOrFailure = + final latestOrFailure = await FolderEventGetCurrentWorkspaceSetting().send(); final userOrFailure = await getIt().getUser(); - final workspaceSetting = workspaceOrFailure.fold( - (workspaceSettingPB) => workspaceSettingPB, + final latest = latestOrFailure.fold( + (latestPB) => latestPB, (error) => null, ); @@ -35,13 +34,13 @@ class UserProfileBloc extends Bloc { (error) => null, ); - if (workspaceSetting == null || userProfile == null) { + if (latest == null || userProfile == null) { return emit(const UserProfileState.workspaceFailure()); } emit( UserProfileState.success( - workspaceSettings: workspaceSetting, + workspaceSettings: latest, userProfile: userProfile, ), ); @@ -59,7 +58,7 @@ class UserProfileState with _$UserProfileState { const factory UserProfileState.loading() = _Loading; const factory UserProfileState.workspaceFailure() = _WorkspaceFailure; const factory UserProfileState.success({ - required WorkspaceSettingPB workspaceSettings, + required WorkspaceLatestPB workspaceSettings, required UserProfilePB userProfile, }) = _Success; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 9497774298..85a5e3cbfa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud if (userProfile == null || - userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + userProfile.authType != AuthenticatorPB.AppFlowyCloud) { return []; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index e6d2d895b1..0e7a7cb4c6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -31,9 +31,9 @@ class MobileFavoriteScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) { - return workspaceSettingPB as WorkspaceSettingPB?; + final latest = snapshots.data?[0].fold( + (latest) { + return latest as WorkspaceLatestPB?; }, (error) => null, ); @@ -46,7 +46,7 @@ class MobileFavoriteScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (latest == null || userProfile == null) { return const WorkspaceFailedScreen(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 1ae5d881ce..fdea8322c3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -44,9 +44,9 @@ class MobileHomeScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) { - return workspaceSettingPB as WorkspaceSettingPB?; + final workspaceLatest = snapshots.data?[0].fold( + (workspaceLatestPB) { + return workspaceLatestPB as WorkspaceLatestPB?; }, (error) => null, ); @@ -59,7 +59,7 @@ class MobileHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (workspaceLatest == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -78,7 +78,7 @@ class MobileHomeScreen extends StatelessWidget { value: userProfile, child: MobileHomePage( userProfile: userProfile, - workspaceSetting: workspaceSetting, + workspaceLatest: workspaceLatest, ), ), ), @@ -95,11 +95,11 @@ class MobileHomePage extends StatefulWidget { const MobileHomePage({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceLatest, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceLatest; @override State createState() => _MobileHomePageState(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index 521fca4fdf..11a82d2c7a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 7ebfeefbbc..cc3f4c8c56 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -167,8 +167,7 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 20, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index a8055b8ba2..33c2eb3905 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -50,9 +50,9 @@ class _MobileNotificationsScreenState extends State orElse: () => const Center(child: CircularProgressIndicator.adaptive()), workspaceFailure: () => const WorkspaceFailedScreen(), - success: (workspaceSetting, userProfile) => + success: (workspaceLatest, userProfile) => _NotificationScreenContent( - workspaceSetting: workspaceSetting, + workspaceLatest: workspaceLatest, userProfile: userProfile, controller: controller, reminderBloc: reminderBloc, @@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State class _NotificationScreenContent extends StatelessWidget { const _NotificationScreenContent({ - required this.workspaceSetting, + required this.workspaceLatest, required this.userProfile, required this.controller, required this.reminderBloc, }); - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceLatest; final UserProfilePB userProfile; final TabController controller; final ReminderBloc reminderBloc; @@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget { ..add( SidebarSectionsEvent.initial( userProfile, - workspaceSetting.workspaceId, + workspaceLatest.workspaceId, ), ), child: BlocBuilder( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index 617de1db50..09f38223f4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget { // delete account button // only show the delete account button in cloud mode - if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index ae0b9173c7..53dc2bd6d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -30,9 +30,9 @@ class DatabaseSyncBloc extends Bloc { .then((value) => value.fold((s) => s, (f) => null)); emit( state.copyWith( - shouldShowIndicator: userProfile?.authenticator == - AuthenticatorPB.AppFlowyCloud && - databaseId != null, + shouldShowIndicator: + userProfile?.authType == AuthenticatorPB.AppFlowyCloud && + databaseId != null, ), ); if (databaseId != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 8d64c537c3..5706167f1a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -69,7 +69,7 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.authenticator ?? AuthenticatorPB.Local) == + (widget.userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 010dae1f12..252742aa5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -101,7 +101,7 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local; + final type = userProfilePB?.authType ?? AuthenticatorPB.Local; return type == AuthenticatorPB.Local; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart index 74a6199b89..b593ccc6cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc emit( state.copyWith( shouldShowIndicator: - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, + userProfile?.authType == AuthenticatorPB.AppFlowyCloud, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 0fae90920d..1001aaef5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, + userProfile?.authType == AuthenticatorPB.AppFlowyCloud, ), ); _syncStateListener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index 83debdd71b..d3bbbd27d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -184,8 +184,8 @@ Future insertLocalFile( final fileType = file.fileType.toMediaFileTypePB(); // Check upload type - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; String? path; String? errorMsg; @@ -229,8 +229,8 @@ Future insertLocalFiles( if (files.every((f) => f.path.isEmpty)) return; // Check upload type - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 27498cc65e..601ba8fa20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -226,7 +226,7 @@ class PageStyleCoverImage extends StatelessWidget { (f) => null, ); final isAppFlowyCloud = - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud; + userProfile?.authType == AuthenticatorPB.AppFlowyCloud; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index a852fa5e38..c3f04a8837 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.authenticator == AuthenticatorPB.AppFlowyCloud, + (v) => v.authType == AuthenticatorPB.AppFlowyCloud, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index 2974156a2a..f39fdb01dc 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -294,7 +294,7 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == + final isLocalMode = (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index b326276c56..7f9a2df329 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -51,7 +51,6 @@ GoRouter generateRouter(Widget child) { // Routes in both desktop and mobile _signInScreenRoute(), _skipLogInScreenRoute(), - _encryptSecretScreenRoute(), _workspaceErrorScreenRoute(), // Desktop only if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(), @@ -471,23 +470,6 @@ GoRoute _workspaceErrorScreenRoute() { ); } -GoRoute _encryptSecretScreenRoute() { - return GoRoute( - path: EncryptSecretScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - return CustomTransitionPage( - child: EncryptSecretScreen( - user: args[EncryptSecretScreen.argUser], - key: args[EncryptSecretScreen.argKey], - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - GoRoute _skipLogInScreenRoute() { return GoRoute( path: SkipLogInScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart deleted file mode 100644 index 19b8101ae8..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/plugins/database/application/defines.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'auth/auth_service.dart'; - -part 'encrypt_secret_bloc.freezed.dart'; - -class EncryptSecretBloc extends Bloc { - EncryptSecretBloc({required this.user}) - : super(EncryptSecretState.initial()) { - _dispatch(); - } - - final UserProfilePB user; - - void _dispatch() { - on((event, emit) async { - await event.when( - setEncryptSecret: (secret) async { - if (isLoading()) { - return; - } - - final payload = UserSecretPB.create() - ..encryptionSecret = secret - ..encryptionSign = user.encryptionSign - ..encryptionType = user.encryptionType - ..userId = user.id; - final result = await UserEventSetEncryptionSecret(payload).send(); - if (!isClosed) { - add(EncryptSecretEvent.didFinishCheck(result)); - } - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - successOrFail: null, - ), - ); - }, - cancelInputSecret: () async { - await getIt().signOut(); - emit( - state.copyWith( - successOrFail: null, - isSignOut: true, - ), - ); - }, - didFinishCheck: (result) { - result.fold( - (unit) { - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - successOrFail: result, - ), - ); - }, - (err) { - emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.failure(err)), - successOrFail: result, - ), - ); - }, - ); - }, - ); - }); - } - - bool isLoading() { - final loadingState = state.loadingState; - if (loadingState != null) { - return loadingState.when( - loading: () => true, - finish: (_) => false, - idle: () => false, - ); - } - return false; - } -} - -@freezed -class EncryptSecretEvent with _$EncryptSecretEvent { - const factory EncryptSecretEvent.setEncryptSecret(String secret) = - _SetEncryptSecret; - const factory EncryptSecretEvent.didFinishCheck( - FlowyResult result, - ) = _DidFinishCheck; - const factory EncryptSecretEvent.cancelInputSecret() = _CancelInputSecret; -} - -@freezed -class EncryptSecretState with _$EncryptSecretState { - const factory EncryptSecretState({ - required FlowyResult? successOrFail, - required bool isSignOut, - LoadingState? loadingState, - }) = _EncryptSecretState; - - factory EncryptSecretState.initial() => const EncryptSecretState( - successOrFail: null, - isSignOut: false, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart index 34e8514e4c..d421440d08 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -46,7 +46,7 @@ class PasswordBloc extends Bloc { bool _isInitialized = false; Future _init() async { - if (userProfile.authenticator == AuthenticatorPB.Local) { + if (userProfile.authType == AuthenticatorPB.Local) { Log.debug('PasswordBloc: skip init because user is local authenticator'); return; } diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 36d6039d40..d3ebe0201b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -24,7 +24,7 @@ typedef DidUpdateUserWorkspacesCallback = void Function( ); typedef UserProfileNotifyValue = FlowyResult; typedef DidUpdateUserWorkspaceSetting = void Function( - UseAISettingPB settings, + WorkspaceSettingsPB settings, ); class UserListener { @@ -101,10 +101,10 @@ class UserListener { result.map( (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)), ); - case user.UserNotification.DidUpdateAISetting: + case user.UserNotification.DidUpdateWorkspaceSetting: result.map( - (r) => - onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)), + (r) => onUserWorkspaceSettingUpdated + ?.call(WorkspaceSettingsPB.fromBuffer(r)), ); break; default: @@ -113,22 +113,21 @@ class UserListener { } } -typedef WorkspaceSettingNotifyValue - = FlowyResult; +typedef WorkspaceLatestNotifyValue = FlowyResult; class FolderListener { FolderListener(); - final PublishNotifier _settingChangedNotifier = + final PublishNotifier _latestChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, + void Function(WorkspaceLatestNotifyValue)? onLatestUpdated, }) { - if (onSettingUpdated != null) { - _settingChangedNotifier.addPublishListener(onSettingUpdated); + if (onLatestUpdated != null) { + _latestChangedNotifier.addPublishListener(onLatestUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -146,9 +145,9 @@ class FolderListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier.value = - FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), - (error) => _settingChangedNotifier.value = FlowyResult.failure(error), + (payload) => _latestChangedNotifier.value = + FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)), + (error) => _latestChangedNotifier.value = FlowyResult.failure(error), ); break; default: @@ -158,6 +157,6 @@ class FolderListener { Future stop() async { await _listener?.stop(); - _settingChangedNotifier.dispose(); + _latestChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 4359a23753..18f89ebc14 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -40,8 +40,6 @@ class UserBackendService implements IUserBackendService { String? password, String? email, String? iconUrl, - String? openAIKey, - String? stabilityAiKey, }) { final payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -61,14 +59,6 @@ class UserBackendService implements IUserBackendService { payload.iconUrl = iconUrl; } - if (openAIKey != null) { - payload.openaiKey = openAIKey; - } - - if (stabilityAiKey != null) { - payload.stabilityAiKey = stabilityAiKey; - } - return UserEventUpdateUserProfile(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index a9b11cb42e..014c7caaf8 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,9 +74,8 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = - isSelected || user.authenticator != AuthenticatorPB.Local; - final desc = "${user.name}\t ${user.authenticator}\t"; + final isDisabled = isSelected || user.authType != AuthenticatorPB.Local; + final desc = "${user.name}\t ${user.authType}\t"; final child = SizedBox( height: 30, child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart index 9abd417df3..83007786f1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart @@ -12,11 +12,7 @@ void handleUserProfileResult( ) { userProfileResult.fold( (userProfile) { - if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { - authRouter.pushEncryptionScreen(context, userProfile); - } else { - authRouter.goHomeScreen(context, userProfile); - } + authRouter.goHomeScreen(context, userProfile); }, (error) { handleOpenWorkspaceError(context, error); diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 370d9c2062..f6f6ec3e3a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -61,20 +61,6 @@ class AuthRouter { ); } - void pushEncryptionScreen( - BuildContext context, - UserProfilePB userProfile, - ) { - // After log in,push EncryptionScreen on the top SignInScreen - context.push( - EncryptSecretScreen.routeName, - extra: { - EncryptSecretScreen.argUser: userProfile, - EncryptSecretScreen.argKey: ValueKey(userProfile.id), - }, - ); - } - Future pushWorkspaceErrorScreen( BuildContext context, UserFolderPB userFolder, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart deleted file mode 100644 index f0b79ed9d2..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/helpers/helpers.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/encrypt_secret_bloc.dart'; - -class EncryptSecretScreen extends StatefulWidget { - const EncryptSecretScreen({required this.user, super.key}); - - final UserProfilePB user; - - static const routeName = '/EncryptSecretScreen'; - - // arguments used in GoRouter - static const argUser = 'user'; - static const argKey = 'key'; - - @override - State createState() => _EncryptSecretScreenState(); -} - -class _EncryptSecretScreenState extends State { - final TextEditingController _textEditingController = TextEditingController(); - - @override - void dispose() { - _textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: BlocProvider( - create: (context) => EncryptSecretBloc(user: widget.user), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (previous, current) => - previous.isSignOut != current.isSignOut, - listener: (context, state) async { - if (state.isSignOut) { - await runAppFlowy(); - } - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous.successOrFail != current.successOrFail, - listener: (context, state) async { - await state.successOrFail?.fold( - (unit) async { - await runAppFlowy(); - }, - (error) { - handleOpenWorkspaceError(context, error); - }, - ); - }, - ), - ], - child: BlocBuilder( - builder: (context, state) { - final indicator = state.loadingState?.when( - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - finish: (result) => const SizedBox.shrink(), - idle: () => const SizedBox.shrink(), - ) ?? - const SizedBox.shrink(); - return Center( - child: SizedBox( - width: 300, - height: 160, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.6, - child: FlowyText.medium( - "${LocaleKeys.settings_menu_inputEncryptPrompt.tr()} ${widget.user.email}", - fontSize: 14, - maxLines: 10, - ), - ), - const VSpace(6), - SizedBox( - width: 300, - child: FlowyTextField( - controller: _textEditingController, - hintText: - LocaleKeys.settings_menu_inputTextFieldHint.tr(), - onChanged: (_) {}, - ), - ), - OkCancelButton( - alignment: MainAxisAlignment.end, - onOkPressed: () => - context.read().add( - EncryptSecretEvent.setEncryptSecret( - _textEditingController.text, - ), - ), - onCancelPressed: () => context - .read() - .add(const EncryptSecretEvent.cancelInputSecret()), - mode: TextButtonMode.normal, - ), - const VSpace(6), - indicator, - ], - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 088da38978..540da8c2b4 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -2,6 +2,5 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; export 'sign_up_screen.dart'; -export 'encrypt_secret_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index afae06d50a..b359b2e217 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -4,7 +4,6 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -34,11 +33,7 @@ class SignInScreen extends StatelessWidget { if (successOrFail != null) { successOrFail.fold( (userProfile) { - if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { - getIt().pushEncryptionScreen(context, userProfile); - } else { - getIt().goHomeScreen(context, userProfile); - } + getIt().goHomeScreen(context, userProfile); }, (error) { Log.error('Sign in error: $error'); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 71345aa8dd..4062cedf8e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -8,7 +8,6 @@ import 'package:appflowy/user/presentation/helpers/helpers.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -61,32 +60,15 @@ class SplashScreen extends StatelessWidget { BuildContext context, Authenticated authenticated, ) async { - final userProfile = authenticated.userProfile; - - /// After a user is authenticated, this function checks if encryption is required. - final result = await UserEventCheckEncryptionSign().send(); - await result.fold( - (check) async { - /// If encryption is needed, the user is navigated to the encryption screen. - /// Otherwise, it fetches the current workspace for the user and navigates them - if (check.requireSecret) { - getIt().pushEncryptionScreen(context, userProfile); - } else { - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (workspaceSetting) { - // After login, replace Splash screen by corresponding home screen - getIt().goHomeScreen( - context, - ); - }, - (error) => handleOpenWorkspaceError(context, error), - ); - } - }, - (err) { - Log.error(err); + final result = await FolderEventGetCurrentWorkspaceSetting().send(); + result.fold( + (workspaceSetting) { + // After login, replace Splash screen by corresponding home screen + getIt().goHomeScreen( + context, + ); }, + (error) => handleOpenWorkspaceError(context, error), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 1afc253ab7..531e797ff5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -3,14 +3,14 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceSettingPB; + show WorkspaceLatestPB; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { - HomeBloc(WorkspaceSettingPB workspaceSetting) + HomeBloc(WorkspaceLatestPB workspaceSetting) : _workspaceListener = FolderListener(), super(HomeState.initial(workspaceSetting)) { _dispatch(workspaceSetting); @@ -24,7 +24,7 @@ class HomeBloc extends Bloc { return super.close(); } - void _dispatch(WorkspaceSettingPB workspaceSetting) { + void _dispatch(WorkspaceLatestPB workspaceSetting) { on( (event, emit) async { await event.map( @@ -36,10 +36,9 @@ class HomeBloc extends Bloc { }); _workspaceListener.start( - onSettingUpdated: (result) { + onLatestUpdated: (result) { result.fold( - (setting) => - add(HomeEvent.didReceiveWorkspaceSetting(setting)), + (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)), (r) => Log.error(r), ); }, @@ -78,7 +77,7 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; const factory HomeEvent.didReceiveWorkspaceSetting( - WorkspaceSettingPB setting, + WorkspaceLatestPB setting, ) = _DidReceiveWorkspaceSetting; } @@ -86,11 +85,11 @@ class HomeEvent with _$HomeEvent { class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, ViewPB? latestView, }) = _HomeState; - factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( + factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState( isLoading: false, workspaceSetting: workspaceSetting, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart index 657f2592d7..cde67045b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart @@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_context.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceSettingPB; + show WorkspaceLatestPB; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +12,7 @@ part 'home_setting_bloc.freezed.dart'; class HomeSettingBloc extends Bloc { HomeSettingBloc( - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, double screenWidthPx, ) : _listener = FolderListener(), @@ -124,7 +124,7 @@ class HomeSettingEvent with _$HomeSettingEvent { _ShowEditPanel; const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel; const factory HomeSettingEvent.didReceiveWorkspaceSetting( - WorkspaceSettingPB setting, + WorkspaceLatestPB setting, ) = _DidReceiveWorkspaceSetting; const factory HomeSettingEvent.collapseMenu() = _CollapseMenu; const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) = @@ -139,7 +139,7 @@ class HomeSettingEvent with _$HomeSettingEvent { class HomeSettingState with _$HomeSettingState { const factory HomeSettingState({ required EditPanelContext? panelContext, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, required bool keepMenuCollapsed, @@ -150,7 +150,7 @@ class HomeSettingState with _$HomeSettingState { }) = _HomeSettingState; factory HomeSettingState.initial( - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, AppearanceSettingsState appearanceSettingsState, double screenWidthPx, ) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 4383e0dbef..0141283765 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -55,7 +55,7 @@ class SettingsAIBloc extends Bloc { onProfileUpdated: _onProfileUpdated, onUserWorkspaceSettingUpdated: (settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadAISetting(settings)); + add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); } }, ); @@ -85,7 +85,7 @@ class SettingsAIBloc extends Bloc { ), ).send(); }, - didLoadAISetting: (UseAISettingPB settings) { + didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) { emit( state.copyWith( aiSettings: settings, @@ -150,7 +150,7 @@ class SettingsAIBloc extends Bloc { UserEventGetWorkspaceSetting(payload).send().then((result) { result.fold((settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadAISetting(settings)); + add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); } }, (err) { Log.error(err); @@ -162,8 +162,8 @@ class SettingsAIBloc extends Bloc { @freezed class SettingsAIEvent with _$SettingsAIEvent { const factory SettingsAIEvent.started() = _Started; - const factory SettingsAIEvent.didLoadAISetting( - UseAISettingPB settings, + const factory SettingsAIEvent.didLoadWorkspaceSetting( + WorkspaceSettingsPB settings, ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; @@ -183,7 +183,7 @@ class SettingsAIEvent with _$SettingsAIEvent { class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, - UseAISettingPB? aiSettings, + WorkspaceSettingsPB? aiSettings, AvailableModelsPB? availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 0578d9808b..66277cf30b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -92,7 +92,7 @@ class SettingsDialogBloc ]) async { if ([ AuthenticatorPB.Local, - ].contains(userProfile.authenticator)) { + ].contains(userProfile.authType)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 7f32a86d1c..e6a8c4d921 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && + userProfile.authType == AuthenticatorPB.AppFlowyCloud && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index a8d768aa79..619ee4e229 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -52,8 +52,8 @@ class DesktopHomeScreen extends StatelessWidget { return _buildLoading(); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB, + final workspaceLatest = snapshots.data?[0].fold( + (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB, (error) => null, ); @@ -64,7 +64,7 @@ class DesktopHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (workspaceLatest == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -86,11 +86,11 @@ class DesktopHomeScreen extends StatelessWidget { BlocProvider.value(value: getIt()), BlocProvider( create: (_) => - HomeBloc(workspaceSetting)..add(const HomeEvent.initial()), + HomeBloc(workspaceLatest)..add(const HomeEvent.initial()), ), BlocProvider( create: (_) => HomeSettingBloc( - workspaceSetting, + workspaceLatest, context.read(), context.widthPx, )..add(const HomeSettingEvent.initial()), @@ -137,7 +137,7 @@ class DesktopHomeScreen extends StatelessWidget { child: _buildBody( context, userProfile, - workspaceSetting, + workspaceLatest, ), ), ), @@ -157,7 +157,7 @@ class DesktopHomeScreen extends StatelessWidget { Widget _buildBody( BuildContext context, UserProfilePB userProfile, - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, ) { final layout = HomeLayout(context); final homeStack = HomeStack( @@ -190,7 +190,7 @@ class DesktopHomeScreen extends StatelessWidget { BuildContext context, { required HomeLayout layout, required UserProfilePB userProfile, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, }) { final homeMenu = HomeSideBar( userProfile: userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index ea55c72f16..9c19184217 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -60,7 +60,7 @@ class HomeSideBar extends StatelessWidget { final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceSetting; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index d792c54f04..576ff07cb4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + if (userProfile.authType != AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 89066ea649..78f1aaf16e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -79,9 +79,7 @@ class AccountSignInOutButton extends StatelessWidget { showConfirmDialog( context: context, title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - description: userProfile.encryptionType == EncryptionTypePB.Symmetric - ? LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr() - : LocaleKeys.settings_menu_logoutPrompt.tr(), + description: LocaleKeys.settings_menu_logoutPrompt.tr(), onConfirm: () async { await getIt().signOut(); onAction(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index e223b2d063..f1ecfda8ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + state.userProfile.authType != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ @@ -82,30 +82,30 @@ class _SettingsAccountViewState extends State { ), AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authenticator == - AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.authenticator == - AuthenticatorPB.Local, + onAction: + state.userProfile.authType == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: + state.userProfile.authType == AuthenticatorPB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.authenticator == AuthenticatorPB.Local) ...[ + state.userProfile.authType == AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authenticator == - AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.authenticator == - AuthenticatorPB.Local, + onAction: + state.userProfile.authType == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: + state.userProfile.authType == AuthenticatorPB.Local, ), ], ), @@ -120,8 +120,7 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 1cfc833398..a602849527 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.authenticator != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.authenticator != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 512d673407..b17a9beb7e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -140,7 +140,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { + if (user.authType == AuthenticatorPB.AppFlowyCloud) { return SettingsAIView( key: ValueKey(workspaceId), userProfile: user, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index cf51d7a3e9..8a85377efe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -2,7 +2,6 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -64,12 +63,8 @@ class SettingThirdPartyLogin extends StatelessWidget { ) async { result.fold( (user) async { - if (user.encryptionType == EncryptionTypePB.Symmetric) { - getIt().pushEncryptionScreen(context, user); - } else { - didLogin(); - await runAppFlowy(); - } + didLogin(); + await runAppFlowy(); }, (error) => showSnapBar(context, error.msg), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index c9069b8be3..1a7144993f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -63,8 +63,7 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + userProfile.authType == AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, @@ -110,8 +109,7 @@ class SettingsMenu extends StatelessWidget { ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 90e47e7c19..3b81d2a041 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { + userProfile.authType == AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index d6d0351414..41865b7dd7 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -182,14 +182,14 @@ void main() { await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); - var workspaceSetting = + var workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( (result) => result.fold( (l) => l, (r) => throw Exception(), ), ); - workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id; + workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id; // ignore: unused_local_variable final documentBloc = DocumentBloc(documentId: document.id) @@ -198,14 +198,13 @@ void main() { ); await blocResponseFuture(); - workspaceSetting = - await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceSetting.latestView.id == document.id; + workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( + (result) => result.fold( + (l) => l, + (r) => throw Exception(), + ), + ); + workspaceLatest.latestView.id == document.id; }); test('create views', () async { diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 02efc0f75a..1b54087ee1 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -8,7 +8,6 @@ use collab_entity::CollabType; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_server::AppFlowyServer; use flowy_user::entities::AuthenticatorPB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; @@ -113,16 +112,25 @@ impl EventIntegrationTest { self.appflowy_core.config.application_path.clone() } - pub fn get_server(&self) -> Arc { - self.appflowy_core.server_provider.get_server().unwrap() - } - pub async fn wait_ws_connected(&self) { - if self.get_server().get_ws_state().is_connected() { + if self + .appflowy_core + .server_provider + .get_server() + .unwrap() + .get_ws_state() + .is_connected() + { return; } - let mut ws_state = self.get_server().subscribe_ws_state().unwrap(); + let mut ws_state = self + .appflowy_core + .server_provider + .get_server() + .unwrap() + .subscribe_ws_state() + .unwrap(); loop { select! { _ = sleep(Duration::from_secs(20)) => { @@ -144,9 +152,10 @@ impl EventIntegrationTest { oid: &str, collab_type: CollabType, ) -> Result, FlowyError> { - let server = self.server_provider.get_server().unwrap(); + let server = self.server_provider.get_server()?; + let workspace_id = self.get_current_workspace().await.id; - let oid = Uuid::from_str(oid).unwrap(); + let oid = Uuid::from_str(oid)?; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 1b82d9b83c..2ec74bafa6 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -24,6 +24,7 @@ use flowy_user::entities::{ }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; +use flowy_user_pub::entities::AuthType; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; @@ -189,9 +190,10 @@ impl EventIntegrationTest { } } - pub async fn create_workspace(&self, name: &str) -> UserWorkspacePB { + pub async fn create_workspace(&self, name: &str, auth_type: AuthType) -> UserWorkspacePB { let payload = CreateWorkspacePB { name: name.to_string(), + auth_type: auth_type.into(), }; EventBuilder::new(self.clone()) .event(UserEvent::CreateWorkspace) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index a9c928db5b..aacba827c4 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -19,7 +19,12 @@ async fn af_cloud_create_chat_message_test() { let current_workspace = test.get_current_workspace().await; let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test.server_provider.get_server().unwrap().chat_service(); + let chat_service = test + .appflowy_core + .server_provider + .get_server() + .unwrap() + .chat_service(); for i in 0..10 { let _ = chat_service .create_question( @@ -74,7 +79,12 @@ async fn af_cloud_load_remote_system_message_test() { let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test.server_provider.get_server().unwrap().chat_service(); + let chat_service = test + .appflowy_core + .server_provider + .get_server() + .unwrap() + .chat_service(); for i in 0..10 { let _ = chat_service .create_question( diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs index 04798f044a..7d8ecc9680 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs @@ -66,6 +66,7 @@ async fn af_cloud_upload_big_file_test() { // download the file and then compare the data. let file_service = test + .appflowy_core .server_provider .get_server() .unwrap() diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 718bc1d9af..f74b202860 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.authenticator, AuthenticatorPB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthenticatorPB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs index 7b31babd0e..eaec8f7540 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs @@ -1,6 +1,5 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; -use flowy_user::entities::UpdateUserProfilePayloadPB; use crate::util::generate_test_email; @@ -13,29 +12,3 @@ async fn af_cloud_sign_up_test() { let user = test.af_cloud_sign_in_with_email(&email).await.unwrap(); assert_eq!(user.email, email); } - -#[tokio::test] -async fn af_cloud_update_user_metadata() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - let user = test.af_cloud_sign_up().await; - - let old_profile = test.get_user_profile().await.unwrap(); - assert_eq!(old_profile.openai_key, "".to_string()); - - test - .update_user_profile(UpdateUserProfilePayloadPB { - id: user.id, - openai_key: Some("new openai key".to_string()), - stability_ai_key: Some("new stability ai key".to_string()), - ..Default::default() - }) - .await; - - let new_profile = test.get_user_profile().await.unwrap(); - assert_eq!(new_profile.openai_key, "new openai key".to_string()); - assert_eq!( - new_profile.stability_ai_key, - "new stability ai key".to_string() - ); -} diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index 56cf22a4da..366e43c157 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -1,15 +1,15 @@ +use crate::user::af_cloud_test::util::get_synced_workspaces; use collab::core::collab::DataSource::DocStateV1; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::Folder; use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; +use flowy_user_pub::entities::AuthType; use std::time::Duration; use tokio::task::LocalSet; use tokio::time::sleep; -use crate::user::af_cloud_test::util::get_synced_workspaces; - #[tokio::test] async fn af_cloud_workspace_delete() { use_localhost_af_cloud().await; @@ -18,7 +18,9 @@ async fn af_cloud_workspace_delete() { let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 1); - let created_workspace = test.create_workspace("my second workspace").await; + let created_workspace = test + .create_workspace("my second workspace", AuthType::AppFlowyCloud) + .await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); @@ -66,7 +68,9 @@ async fn af_cloud_create_workspace_test() { let first_workspace_id = workspaces[0].workspace_id.as_str(); assert_eq!(workspaces.len(), 1); - let created_workspace = test.create_workspace("my second workspace").await; + let created_workspace = test + .create_workspace("my second workspace", AuthType::AppFlowyCloud) + .await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; @@ -113,7 +117,9 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[2].name, "A"); assert_eq!(views[3].name, "B"); - let user_workspace = test.create_workspace("second workspace").await; + let user_workspace = test + .create_workspace("second workspace", AuthType::AppFlowyCloud) + .await; test.open_workspace(&user_workspace.workspace_id).await; let second_workspace = test.get_current_workspace().await; test.create_document("C").await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs index 00df14e8e1..46f8eca9c6 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs @@ -24,9 +24,7 @@ async fn anon_user_profile_get() { .await .parse::(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.openai_key, user.openai_key); - assert_eq!(user_profile.stability_ai_key, user.stability_ai_key); - assert_eq!(user_profile.authenticator, AuthenticatorPB::Local); + assert_eq!(user_profile.auth_type, AuthenticatorPB::Local); } #[tokio::test] @@ -50,31 +48,6 @@ async fn user_update_with_name() { assert_eq!(user_profile.name, new_name,); } -#[tokio::test] -async fn user_update_with_ai_key() { - let sdk = EventIntegrationTest::new().await; - let user = sdk.init_anon_user().await; - let openai_key = "openai_key".to_owned(); - let stability_ai_key = "stability_ai_key".to_owned(); - let request = UpdateUserProfilePayloadPB::new(user.id) - .openai_key(&openai_key) - .stability_ai_key(&stability_ai_key); - let _ = EventBuilder::new(sdk.clone()) - .event(UpdateUserProfile) - .payload(request) - .async_send() - .await; - - let user_profile = EventBuilder::new(sdk.clone()) - .event(GetUserProfile) - .async_send() - .await - .parse::(); - - assert_eq!(user_profile.openai_key, openai_key,); - assert_eq!(user_profile.stability_ai_key, stability_ai_key,); -} - #[tokio::test] async fn anon_user_update_with_email() { let sdk = EventIntegrationTest::new().await; diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 85f2dc8306..f85858b1c2 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -330,7 +330,7 @@ pub(crate) async fn update_chat_settings_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip_all, err)] +#[tracing::instrument(level = "debug", skip_all)] pub(crate) async fn get_local_ai_setting_handler( ai_manager: AFPluginState>, ) -> DataResult { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index d217f52785..7e6d477407 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::LoginUserService; +use flowy_server::af_cloud::define::LoggedUser; use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, ServerProvider}; +use crate::server_layer::ServerProvider; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -131,12 +131,10 @@ impl AppFlowyCore { store_preference.clone(), )); - let auth_type = current_server_type(); - debug!("🔥runtime:{}, server:{}", runtime, auth_type); + debug!("🔥runtime:{}", runtime); let server_provider = Arc::new(ServerProvider::new( config.clone(), - auth_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -327,7 +325,7 @@ impl ServerUserImpl { } #[async_trait] -impl LoginUserService for ServerUserImpl { +impl LoggedUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index ebb86c4417..4b1834c41c 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,11 +1,12 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; use arc_swap::{ArcSwap, ArcSwapOption}; +use dashmap::mapref::one::Ref; use dashmap::DashMap; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::{ - define::{AIUserServiceImpl, LoginUserService}, + define::{AIUserServiceImpl, LoggedUser}, AppFlowyCloudServer, }; use flowy_server::local_server::LocalServer; @@ -13,31 +14,53 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; +use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +use tracing::info; pub struct ServerProvider { config: AppFlowyCoreConfig, providers: DashMap>, auth_type: ArcSwap, - user: Arc, + logged_user: Arc, pub local_ai: Arc, pub uid: Arc>, pub user_enable_sync: Arc, pub encryption: Arc, } +// Our little guard wrapper: +pub struct ServerHandle<'a>(Ref<'a, AuthType, Arc>); + +impl<'a> Deref for ServerHandle<'a> { + type Target = dyn AppFlowyServer; + fn deref(&self) -> &Self::Target { + // `self.0.value()` is an `&Arc` + // so `&**` gives us a `&dyn AppFlowyServer` + &**self.0.value() + } +} + +/// Determine current server type from ENV +pub fn current_server_type() -> AuthType { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, + } +} + impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - initial_auth: AuthType, store_preferences: Weak, - user_service: impl LoginUserService + 'static, + user_service: impl LoggedUser + 'static, ) -> Self { - let user = Arc::new(user_service); + let initial_auth = current_server_type(); + let logged_user = Arc::new(user_service) as Arc; let auth_type = ArcSwap::from(Arc::new(initial_auth)); let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; - let ai_user = Arc::new(AIUserServiceImpl(user.clone())); + let ai_user = Arc::new(AIUserServiceImpl(Arc::downgrade(&logged_user))); let plugins = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( plugins, @@ -51,7 +74,7 @@ impl ServerProvider { encryption, user_enable_sync: Arc::new(AtomicBool::new(true)), auth_type, - user, + logged_user, uid: Default::default(), local_ai, } @@ -59,9 +82,16 @@ impl ServerProvider { pub fn set_auth_type(&self, new_auth_type: AuthType) { let old_type = self.get_auth_type(); + info!( + "ServerProvider: set auth type from {:?} to {:?}", + old_type, new_auth_type + ); + if old_type != new_auth_type { self.auth_type.store(Arc::new(new_auth_type)); - self.providers.remove(&old_type); + if let Some((auth_type, _)) = self.providers.remove(&old_type) { + info!("ServerProvider: remove old auth type: {:?}", auth_type); + } } } @@ -70,14 +100,17 @@ impl ServerProvider { } /// Lazily create or fetch an AppFlowyServer instance - pub fn get_server(&self) -> FlowyResult> { + pub fn get_server(&self) -> FlowyResult { let auth_type = self.get_auth_type(); - if let Some(entry) = self.providers.get(&auth_type) { - return Ok(entry.clone()); + if let Some(r) = self.providers.get(&auth_type) { + return Ok(ServerHandle(r)); } let server: Arc = match auth_type { - AuthType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + AuthType::Local => Arc::new(LocalServer::new( + self.logged_user.clone(), + self.local_ai.clone(), + )), AuthType::AppFlowyCloud => { let cfg = self .config @@ -89,20 +122,13 @@ impl ServerProvider { self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), - self.user.clone(), + Arc::downgrade(&self.logged_user), )) }, }; - self.providers.insert(auth_type, server.clone()); - Ok(server) - } -} - -/// Determine current server type from ENV -pub fn current_server_type() -> AuthType { - match AuthenticatorType::from_env() { - AuthenticatorType::Local => AuthType::Local, - AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, + self.providers.insert(auth_type, server); + let guard = self.providers.get(&auth_type).unwrap(); + Ok(ServerHandle(guard)) } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 3288252ad2..4112883e61 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -380,6 +380,9 @@ pub enum ErrorCode { #[error("Local AI disabled")] LocalAIDisabled = 130, + + #[error("User not login")] + UserNotLogin = 131, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 96b9d1c3cf..a9a2b6fa2b 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -161,6 +161,7 @@ impl FlowyError { static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); static_flowy_error!(local_ai_disabled, ErrorCode::LocalAIDisabled); + static_flowy_error!(user_not_login, ErrorCode::UserNotLogin); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-error/src/impl_from/database.rs b/frontend/rust-lib/flowy-error/src/impl_from/database.rs index 3a72a7cdf3..077ff2b708 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/database.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/database.rs @@ -1,8 +1,12 @@ use crate::FlowyError; +use flowy_sqlite::Error; impl std::convert::From for FlowyError { fn from(error: flowy_sqlite::Error) -> Self { - FlowyError::internal().with_context(error) + match error { + Error::NotFound => FlowyError::record_not_found(), + _ => FlowyError::internal().with_context(error), + } } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 21ff046226..72e50562f3 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -134,7 +134,7 @@ impl TryInto for GetWorkspaceViewPB { } #[derive(Default, ProtoBuf, Debug, Clone)] -pub struct WorkspaceSettingPB { +pub struct WorkspaceLatestPB { #[pb(index = 1)] pub workspace_id: String, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index c20eb8a7ad..6889b7ebe6 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -84,7 +84,7 @@ pub(crate) async fn read_private_views_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; let setting = folder.get_workspace_setting_pb().await?; data_result_ok(setting) diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index abd74bd338..19953aad1b 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -65,7 +65,7 @@ pub enum FolderEvent { CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace - #[event(output = "WorkspaceSettingPB")] + #[event(output = "WorkspaceLatestPB")] GetCurrentWorkspaceSetting = 1, /// Return a list of workspaces that the current user can access. diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index ea89def872..e704be043d 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -3,7 +3,7 @@ use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, - ViewLayoutPB, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, + ViewLayoutPB, ViewPB, ViewSectionPB, WorkspaceLatestPB, WorkspacePB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -377,10 +377,10 @@ impl FolderManager { Ok(new_workspace) } - pub async fn get_workspace_setting_pb(&self) -> FlowyResult { + pub async fn get_workspace_setting_pb(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; - Ok(WorkspaceSettingPB { + Ok(WorkspaceLatestPB { workspace_id: workspace_id.to_string(), latest_view, }) @@ -1262,7 +1262,7 @@ impl FolderManager { } let workspace_id = self.user.workspace_id()?; - let setting = WorkspaceSettingPB { + let setting = WorkspaceLatestPB { workspace_id: workspace_id.to_string(), latest_view: view, }; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 0cebe3371f..a93066054d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -3,7 +3,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -13,7 +13,7 @@ pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. #[async_trait] -pub trait LoginUserService: Send + Sync { +pub trait LoggedUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; @@ -24,27 +24,36 @@ pub trait LoginUserService: Send + Sync { fn application_root_dir(&self) -> Result; } -pub struct AIUserServiceImpl(pub Arc); +pub struct AIUserServiceImpl(pub Weak); + +impl AIUserServiceImpl { + fn logged_user(&self) -> FlowyResult> { + self + .0 + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("User is not logged in")) + } +} #[async_trait] impl AIUserService for AIUserServiceImpl { fn user_id(&self) -> Result { - self.0.user_id() + self.logged_user()?.user_id() } async fn is_local_model(&self) -> FlowyResult { - self.0.is_local_mode().await + self.logged_user()?.is_local_mode().await } fn workspace_id(&self) -> Result { - self.0.workspace_id() + self.logged_user()?.workspace_id() } fn sqlite_connection(&self, uid: i64) -> Result { - self.0.get_sqlite_db(uid) + self.logged_user()?.get_sqlite_db(uid) } fn application_root_dir(&self) -> Result { - self.0.application_root_dir() + self.logged_user()?.application_root_dir() } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index d6a22a2f73..f29a7f89ad 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,5 +1,5 @@ #![allow(unused_variables)] -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ @@ -17,13 +17,13 @@ use flowy_database_pub::cloud::{ use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use serde_json::{Map, Value}; -use std::sync::Arc; +use std::sync::Weak; use tracing::{error, instrument}; use uuid::Uuid; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] @@ -40,7 +40,6 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -50,7 +49,7 @@ where Ok(data) => { check_request_workspace_id_is_match( workspace_id, - &cloned_user, + &self.logged_user, format!("get database object: {}:{}", object_id, collab_type), )?; Ok(Some(data.encode_collab)) @@ -95,14 +94,17 @@ where workspace_id: &Uuid, ) -> Result { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let client = try_get_client?; let params = object_ids .into_iter() .map(|object_id| QueryCollab::new(object_id, object_ty)) .collect(); let results = client.batch_get_collab(workspace_id, params).await?; - check_request_workspace_id_is_match(workspace_id, &cloned_user, "batch get database object")?; + check_request_workspace_id_is_match( + workspace_id, + &self.logged_user, + "batch get database object", + )?; Ok( results .0 diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index ae67fc6d26..1e000d5971 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -9,17 +9,17 @@ use collab_entity::CollabType; use flowy_document_pub::cloud::*; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use std::sync::Arc; +use std::sync::Weak; use tracing::instrument; use uuid::Uuid; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 116651a734..e6408bc24c 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -10,7 +10,7 @@ use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; use serde_json::to_vec; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::Weak; use tracing::{instrument, trace}; use uuid::Uuid; @@ -22,13 +22,13 @@ use flowy_folder_pub::cloud::{ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] @@ -91,7 +91,6 @@ where ) -> Result, FlowyError> { let uid = *uid; let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*workspace_id, CollabType::Folder), @@ -103,7 +102,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder data")?; + check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder data")?; let folder = Folder::from_collab_doc_state( uid, CollabOrigin::Empty, @@ -131,7 +130,6 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -143,7 +141,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder doc state")?; + check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder doc state")?; Ok(doc_state) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 0ac4555d6e..8309e4c65f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use anyhow::anyhow; use arc_swap::ArcSwapOption; @@ -31,7 +31,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use uuid::Uuid; -use crate::af_cloud::define::{LoginUserService, USER_SIGN_IN_URL}; +use crate::af_cloud::define::{LoggedUser, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; @@ -44,19 +44,19 @@ use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_st pub(crate) struct AFCloudUserAuthServiceImpl { server: T, user_change_recv: ArcSwapOption>, - user: Arc, + logged_user: Weak, } impl AFCloudUserAuthServiceImpl { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver, - user: Arc, + logged_user: Weak, ) -> Self { Self { server, user_change_recv: ArcSwapOption::new(Some(Arc::new(user_change_recv))), - user, + logged_user, } } } @@ -168,11 +168,7 @@ where Ok(url) } - async fn update_user( - &self, - _credential: UserCredentials, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; client @@ -187,8 +183,11 @@ where _credential: UserCredentials, ) -> Result { let try_get_client = self.server.try_get_client(); - let cloned_user = self.user.clone(); - let expected_workspace_id = cloned_user.workspace_id()?; + let expected_workspace_id = self + .logged_user + .upgrade() + .ok_or_else(FlowyError::user_not_login)? + .workspace_id()?; let client = try_get_client?; let profile = client.get_profile().await?; let token = client.get_token()?; @@ -196,7 +195,11 @@ where // Discard the response if the user has switched to a new workspace. This avoids updating the // user profile with potentially outdated information when the workspace ID no longer matches. - check_request_workspace_id_is_match(&expected_workspace_id, &cloned_user, "get user profile")?; + check_request_workspace_id_is_match( + &expected_workspace_id, + &self.logged_user, + "get user profile", + )?; Ok(profile) } @@ -233,19 +236,17 @@ where async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_owned(); - let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); - let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); let client = try_get_client?; client .patch_workspace(PatchWorkspaceParam { workspace_id, - workspace_name: owned_workspace_name, - workspace_icon: owned_workspace_icon, + workspace_name: new_workspace_name, + workspace_icon: new_workspace_icon, }) .await?; Ok(()) @@ -363,7 +364,7 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, CollabType::UserAwareness), @@ -435,9 +436,9 @@ where async fn subscribe_workspace( &self, - workspace_id: String, + workspace_id: Uuid, recurring_interval: RecurringInterval, - subscription_plan: SubscriptionPlan, + workspace_subscription_plan: SubscriptionPlan, success_url: String, ) -> Result { let try_get_client = self.server.try_get_client(); @@ -447,7 +448,7 @@ where .create_subscription( &workspace_id, recurring_interval, - subscription_plan, + workspace_subscription_plan, &success_url, ) .await?; @@ -490,11 +491,13 @@ where async fn get_workspace_subscription_one( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let workspace_subscriptions = client.get_workspace_subscriptions(&workspace_id).await?; + let workspace_subscriptions = client + .get_workspace_subscriptions(&workspace_id.to_string()) + .await?; Ok(workspace_subscriptions) } @@ -531,11 +534,13 @@ where async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let usage = client.get_workspace_usage_and_limit(&workspace_id).await?; + let usage = client + .get_workspace_usage_and_limit(&workspace_id.to_string()) + .await?; Ok(usage) } @@ -548,7 +553,7 @@ where async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { @@ -556,7 +561,7 @@ where let client = try_get_client?; client .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { - workspace_id, + workspace_id: workspace_id.to_string(), plan, recurring_interval, }) @@ -573,7 +578,7 @@ where async fn get_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); @@ -584,7 +589,7 @@ where async fn update_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, ) -> Result { trace!("Sync workspace settings: {:?}", workspace_settings); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index eb2bf26698..ba13a7fbca 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -4,20 +4,11 @@ use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFW use flowy_user_pub::entities::{ AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, - USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, + USER_METADATA_ICON_URL, }; -use crate::af_cloud::impls::user::util::encryption_type_from_profile; - pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUserParams { let mut user_metadata = UserMetaData::new(); - if let Some(openai_key) = update.openai_key { - user_metadata.insert(USER_METADATA_OPEN_AI_KEY, openai_key); - } - - if let Some(stability_ai_key) = update.stability_ai_key { - user_metadata.insert(USER_METADATA_STABILITY_AI_KEY, stability_ai_key); - } if let Some(icon_url) = update.icon_url { user_metadata.insert(USER_METADATA_ICON_URL, icon_url); @@ -35,19 +26,12 @@ pub fn user_profile_from_af_profile( token: String, profile: AFUserProfile, ) -> Result { - let encryption_type = encryption_type_from_profile(&profile); - let (icon_url, openai_key, stability_ai_key) = { + let icon_url = { profile .metadata .map(|m| { - ( - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - m.get(USER_METADATA_OPEN_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - m.get(USER_METADATA_STABILITY_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - ) + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) }) .unwrap_or_default() }; @@ -57,13 +41,9 @@ pub fn user_profile_from_af_profile( name: profile.name.unwrap_or("".to_string()), token, icon_url: icon_url.unwrap_or_default(), - openai_key: openai_key.unwrap_or_default(), - stability_ai_key: stability_ai_key.unwrap_or_default(), - authenticator: AuthType::AppFlowyCloud, - encryption_type, + auth_type: AuthType::AppFlowyCloud, uid: profile.uid, updated_at: profile.updated_at, - ai_model: "".to_string(), }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index bedcc90ca0..300738c833 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,6 +1,6 @@ -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use flowy_error::{FlowyError, FlowyResult}; -use std::sync::Arc; +use std::sync::Weak; use tracing::warn; use uuid::Uuid; @@ -9,9 +9,10 @@ use uuid::Uuid; /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( expected_workspace_id: &Uuid, - user: &Arc, + user: &Weak, action: impl AsRef, ) -> FlowyResult<()> { + let user = user.upgrade().ok_or_else(FlowyError::user_not_login)?; let actual_workspace_id = user.workspace_id()?; if expected_workspace_id != &actual_workspace_id { warn!( diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index bb2d11cdbd..66abb32031 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,8 +1,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use std::time::Duration; -use crate::af_cloud::define::{AIUserServiceImpl, LoginUserService}; +use crate::af_cloud::define::{AIUserServiceImpl, LoggedUser}; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -53,7 +53,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc, pub device_id: String, ws_client: Arc, - logged_user: Arc, + logged_user: Weak, } impl AppFlowyCloudServer { @@ -62,7 +62,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - auth_user_service: Arc, + logged_user: Weak, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -91,8 +91,8 @@ impl AppFlowyCloudServer { ); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); - spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); + Self { config, client: api_client, @@ -100,16 +100,17 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - logged_user: auth_user_service, + logged_user, } } - fn get_client(&self) -> Option> { - if self.enable_sync.load(Ordering::SeqCst) { + fn get_server_impl(&self) -> AFServerImpl { + let client = if self.enable_sync.load(Ordering::SeqCst) { Some(self.client.clone()) } else { None - } + }; + AFServerImpl { client } } } @@ -165,9 +166,6 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn user_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; let mut user_change = self.ws_client.subscribe_user_changed(); let (tx, rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { @@ -185,59 +183,45 @@ impl AppFlowyServer for AppFlowyCloudServer { }); Arc::new(AFCloudUserAuthServiceImpl::new( - server, + self.get_server_impl(), rx, self.logged_user.clone(), )) } fn folder_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudFolderCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn database_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn database_ai_service(&self) -> Option> { - let server = AFServerImpl { - client: self.get_client(), - }; Some(Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), })) } fn document_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudDocumentCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn chat_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; - Arc::new(AutoSyncChatService::new( - Arc::new(CloudChatServiceImpl { inner: server }), + Arc::new(CloudChatServiceImpl { + inner: self.get_server_impl(), + }), Arc::new(AIUserServiceImpl(self.logged_user.clone())), )) } @@ -269,21 +253,16 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn file_storage(&self) -> Option> { - let client = AFServerImpl { - client: self.get_client(), - }; Some(Arc::new(AFCloudFileStorageServiceImpl::new( - client, + self.get_server_impl(), self.config.maximum_upload_file_size_in_bytes, ))) } fn search_service(&self) -> Option> { - let server = AFServerImpl { - client: self.get_client(), - }; - - Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server })) + Some(Arc::new(AFCloudSearchCloudServiceImpl { + inner: self.get_server_impl(), + })) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 8e731edb84..f56a8d6e8b 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::CompletionStream; @@ -28,7 +28,7 @@ use tracing::trace; use uuid::Uuid; pub struct LocalChatServiceImpl { - pub user: Arc, + pub user: Arc, pub local_ai: Arc, } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 706e7f0597..49db8e03a2 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -113,11 +113,7 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - async fn update_user( - &self, - _credential: UserCredentials, - _params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { + async fn update_user(&self, _params: UpdateUserProfileParams) -> Result<(), FlowyError> { Ok(()) } @@ -146,8 +142,8 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError> { Err( FlowyError::local_version_not_support() diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 719dd59c95..c81d526acb 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,7 +1,7 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::local_server::impls::{ LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, @@ -17,13 +17,13 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, local_ai, diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index a8dcdb507f..249ff9136d 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; use crate::setup_log; -use flowy_server::af_cloud::define::LoginUserService; +use flowy_server::af_cloud::define::LoggedUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; @@ -30,19 +30,21 @@ pub fn get_af_cloud_config() -> Option { pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc { let fake_device_id = uuid::Uuid::new_v4().to_string(); + let logged_user = Arc::new(FakeServerUserImpl) as Arc; Arc::new(AppFlowyCloudServer::new( config, true, fake_device_id, Version::new(0, 5, 8), - Arc::new(FakeServerUserImpl), + // do nothing, just for test + Arc::downgrade(&logged_user), )) } struct FakeServerUserImpl; #[async_trait] -impl LoginUserService for FakeServerUserImpl { +impl LoggedUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 91c9aa8162..27ccdc8f18 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -89,16 +89,11 @@ diesel::table! { user_table (id) { id -> Text, name -> Text, - workspace -> Text, icon_url -> Text, - openai_key -> Text, token -> Text, email -> Text, auth_type -> Integer, - encryption_type -> Text, - stability_ai_key -> Text, updated_at -> BigInt, - ai_model -> Text, } } @@ -112,6 +107,7 @@ diesel::table! { icon -> Text, member_count -> BigInt, role -> Nullable, + auth_type -> Integer, } } @@ -127,16 +123,25 @@ diesel::table! { } } +diesel::table! { + workspace_setting_table (id) { + id -> Text, + disable_search_indexing -> Bool, + ai_model -> Text, + } +} + diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dd69e4aa37..dee44fa5f3 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -164,11 +164,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result; /// Using the user's token to update the user information - async fn update_user( - &self, - credential: UserCredentials, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError>; + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError>; /// Get the user information using the user's token or uid /// return None if the user is not found @@ -187,8 +183,8 @@ pub trait UserCloudService: Send + Sync + 'static { async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError>; /// Deletes a workspace owned by the user. @@ -277,7 +273,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn subscribe_workspace( &self, - workspace_id: String, + workspace_id: Uuid, recurring_interval: RecurringInterval, workspace_subscription_plan: SubscriptionPlan, success_url: String, @@ -303,7 +299,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Get the workspace subscriptions for a workspace async fn get_workspace_subscription_one( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result, FlowyError> { Err(FlowyError::not_support()) } @@ -326,7 +322,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result { Err(FlowyError::not_support()) } @@ -337,7 +333,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { @@ -350,14 +346,14 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { Err(FlowyError::not_support()) } async fn update_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, ) -> Result { Err(FlowyError::not_support()) diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index d59f9cab47..7c8d775fa6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -10,8 +10,6 @@ use serde_json::Value; use serde_repr::*; use uuid::Uuid; -pub const USER_METADATA_OPEN_AI_KEY: &str = "openai_key"; -pub const USER_METADATA_STABILITY_AI_KEY: &str = "stability_ai_key"; pub const USER_METADATA_ICON_URL: &str = "icon_url"; pub const USER_METADATA_UPDATE_AT: &str = "updated_at"; @@ -171,21 +169,15 @@ impl UserWorkspace { } } -#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct UserProfile { - #[serde(rename = "id")] pub uid: i64, pub email: String, pub name: String, pub token: String, pub icon_url: String, - pub openai_key: String, - pub stability_ai_key: String, - pub authenticator: AuthType, - // If the encryption_sign is not empty, which means the user has enabled the encryption. - pub encryption_type: EncryptionType, + pub auth_type: AuthType, pub updated_at: i64, - pub ai_model: String, } #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] @@ -233,37 +225,23 @@ where { fn from(params: (&T, &AuthType)) -> Self { let (value, auth_type) = params; - let (icon_url, openai_key, stability_ai_key) = { - value - .metadata() - .as_ref() - .map(|m| { - ( - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - m.get(USER_METADATA_OPEN_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - m.get(USER_METADATA_STABILITY_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - ) - }) - .unwrap_or_default() - }; + let icon_url = value + .metadata() + .as_ref() + .map(|m| { + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default() + }) + .unwrap_or_default(); Self { uid: value.user_id(), email: value.user_email().unwrap_or_default(), name: value.user_name().to_owned(), token: value.user_token().unwrap_or_default(), icon_url, - openai_key, - authenticator: *auth_type, - encryption_type: value.encryption_type(), - stability_ai_key, + auth_type: *auth_type, updated_at: value.updated_at(), - ai_model: "".to_string(), } } } @@ -275,11 +253,7 @@ pub struct UpdateUserProfileParams { pub email: Option, pub password: Option, pub icon_url: Option, - pub openai_key: Option, - pub stability_ai_key: Option, - pub encryption_sign: Option, pub token: Option, - pub ai_model: Option, } impl UpdateUserProfileParams { @@ -314,40 +288,6 @@ impl UpdateUserProfileParams { self.icon_url = Some(icon_url.to_string()); self } - - pub fn with_openai_key(mut self, openai_key: &str) -> Self { - self.openai_key = Some(openai_key.to_owned()); - self - } - - pub fn with_stability_ai_key(mut self, stability_ai_key: &str) -> Self { - self.stability_ai_key = Some(stability_ai_key.to_owned()); - self - } - - pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self { - let sign = match encryption_type { - EncryptionType::NoEncryption => "".to_string(), - EncryptionType::SelfEncryption(sign) => sign, - }; - self.encryption_sign = Some(sign); - self - } - - pub fn with_ai_model(mut self, ai_model: &str) -> Self { - self.ai_model = Some(ai_model.to_owned()); - self - } - - pub fn is_empty(&self) -> bool { - self.name.is_none() - && self.email.is_none() - && self.password.is_none() - && self.icon_url.is_none() - && self.openai_key.is_none() - && self.encryption_sign.is_none() - && self.stability_ai_key.is_none() - } } #[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 7e62958c68..54cb41db1e 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -4,11 +4,10 @@ use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey}; +use crate::entities::parser::{UserEmail, UserIcon, UserName}; use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; -use super::parser::UserStabilityAIKey; use super::AFRolePB; #[derive(Default, ProtoBuf)] @@ -41,22 +40,7 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub openai_key: String, - - #[pb(index = 7)] - pub authenticator: AuthenticatorPB, - - #[pb(index = 8)] - pub encryption_sign: String, - - #[pb(index = 9)] - pub encryption_type: EncryptionTypePB, - - #[pb(index = 10)] - pub stability_ai_key: String, - - #[pb(index = 11)] - pub ai_model: String, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -73,23 +57,13 @@ impl Default for EncryptionTypePB { impl From for UserProfilePB { fn from(user_profile: UserProfile) -> Self { - let (encryption_sign, encryption_ty) = match user_profile.encryption_type { - EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), - EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), - }; - let ai_model = user_profile.ai_model; Self { id: user_profile.uid, email: user_profile.email, name: user_profile.name, token: user_profile.token, icon_url: user_profile.icon_url, - openai_key: user_profile.openai_key, - authenticator: user_profile.authenticator.into(), - encryption_sign, - encryption_type: encryption_ty, - stability_ai_key: user_profile.stability_ai_key, - ai_model, + auth_type: user_profile.auth_type.into(), } } } @@ -110,12 +84,6 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 5, one_of)] pub icon_url: Option, - - #[pb(index = 6, one_of)] - pub openai_key: Option, - - #[pb(index = 7, one_of)] - pub stability_ai_key: Option, } impl UpdateUserProfilePayloadPB { @@ -145,16 +113,6 @@ impl UpdateUserProfilePayloadPB { self.icon_url = Some(icon_url.to_owned()); self } - - pub fn openai_key(mut self, openai_key: &str) -> Self { - self.openai_key = Some(openai_key.to_owned()); - self - } - - pub fn stability_ai_key(mut self, stability_ai_key: &str) -> Self { - self.stability_ai_key = Some(stability_ai_key.to_owned()); - self - } } impl TryInto for UpdateUserProfilePayloadPB { @@ -178,27 +136,13 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), }; - let openai_key = match self.openai_key { - None => None, - Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0), - }; - - let stability_ai_key = match self.stability_ai_key { - None => None, - Some(stability_ai_key) => Some(UserStabilityAIKey::parse(stability_ai_key)?.0), - }; - Ok(UpdateUserProfileParams { uid: self.id, name, email, password, icon_url, - openai_key, - encryption_sign: None, token: None, - stability_ai_key, - ai_model: None, }) } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 885ad6f3cf..26f848f3f5 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -5,9 +5,10 @@ use client_api::entity::billing_dto::{ use serde::{Deserialize, Serialize}; use validator::Validate; +use crate::services::sqlite_sql::workspace_setting_sql::WorkspaceSettingsTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; -use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember}; +use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] @@ -215,6 +216,34 @@ pub struct CreateWorkspacePB { #[pb(index = 1)] #[validate(custom(function = "required_not_empty_str"))] pub name: String, + + #[pb(index = 2)] + pub auth_type: AuthTypePB, +} + +#[derive(ProtoBuf_Enum, Default, Clone)] +pub enum AuthTypePB { + LocalAuthType = 0, + #[default] + CloudAuthType = 1, +} + +impl From for AuthTypePB { + fn from(value: AuthType) -> Self { + match value { + AuthType::Local => AuthTypePB::LocalAuthType, + AuthType::AppFlowyCloud => AuthTypePB::CloudAuthType, + } + } +} + +impl From for AuthType { + fn from(value: AuthTypePB) -> Self { + match value { + AuthTypePB::LocalAuthType => AuthType::Local, + AuthTypePB::CloudAuthType => AuthType::AppFlowyCloud, + } + } } #[derive(ProtoBuf, Default, Clone, Validate)] @@ -375,8 +404,8 @@ pub struct BillingPortalPB { pub url: String, } -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct UseAISettingPB { +#[derive(ProtoBuf, Default, Clone, Validate, Eq, PartialEq)] +pub struct WorkspaceSettingsPB { #[pb(index = 1)] pub disable_search_indexing: bool, @@ -384,8 +413,17 @@ pub struct UseAISettingPB { pub ai_model: String, } -impl From for UseAISettingPB { - fn from(value: AFWorkspaceSettings) -> Self { +impl From<&AFWorkspaceSettings> for WorkspaceSettingsPB { + fn from(value: &AFWorkspaceSettings) -> Self { + Self { + disable_search_indexing: value.disable_search_indexing, + ai_model: value.ai_model.clone(), + } + } +} + +impl From for WorkspaceSettingsPB { + fn from(value: WorkspaceSettingsTable) -> Self { Self { disable_search_indexing: value.disable_search_indexing, ai_model: value.ai_model, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 7a64c20e06..bb2a0b2c30 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,6 +1,5 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; @@ -17,6 +16,7 @@ use crate::services::cloud_config::{ get_cloud_config, get_or_create_cloud_config, save_cloud_config, }; use crate::services::data_import::prepare_import; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceChangeset; use crate::user_manager::UserManager; fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { @@ -45,7 +45,7 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_server_auth_type(); + let old_authenticator = manager.cloud_service.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await @@ -53,7 +53,7 @@ pub async fn sign_in_with_email_password_handler( Ok(token) => data_result_ok(token.into()), Err(err) => { manager - .cloud_services + .cloud_service .set_server_auth_type(&old_authenticator); return Err(err); }, @@ -78,11 +78,11 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_services.get_server_auth_type(); + let prev_auth_type = manager.cloud_service.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager.cloud_services.set_server_auth_type(&prev_auth_type); + manager.cloud_service.set_server_auth_type(&prev_auth_type); Err(err) }, } @@ -117,7 +117,7 @@ pub async fn get_user_profile_handler( // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.authenticator == AuthType::Local { + if user_profile.auth_type == AuthType::Local { user_profile.email = "".to_string(); } @@ -374,66 +374,6 @@ pub async fn sign_in_with_provider_handler( }) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn set_encrypt_secret_handler( - manager: AFPluginState>, - data: AFPluginData, - store_preferences: AFPluginState>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let store_preferences = upgrade_store_preferences(store_preferences)?; - let data = data.into_inner(); - match data.encryption_type { - EncryptionTypePB::NoEncryption => { - tracing::error!("Encryption type is NoEncryption, but set encrypt secret"); - }, - EncryptionTypePB::Symmetric => { - manager.check_encryption_sign_with_secret( - data.user_id, - &data.encryption_sign, - &data.encryption_secret, - )?; - - let config = UserCloudConfig::new(data.encryption_secret).with_enable_encrypt(true); - manager - .set_encrypt_secret( - data.user_id, - config.encrypt_secret.clone(), - EncryptionType::SelfEncryption(data.encryption_sign), - ) - .await?; - save_cloud_config(data.user_id, &store_preferences, &config)?; - }, - } - - manager.resume_sign_up().await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn check_encrypt_secret_handler( - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let uid = manager.get_session()?.user_id; - let profile = manager.get_user_profile_from_disk(uid).await?; - - let is_need_secret = match profile.encryption_type { - EncryptionType::NoEncryption => false, - EncryptionType::SelfEncryption(sign) => { - if sign.is_empty() { - false - } else { - manager.check_encryption_sign(uid, &sign).is_err() - } - }, - }; - - data_result_ok(UserEncryptionConfigurationPB { - require_secret: is_need_secret, - }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_cloud_config_handler( manager: AFPluginState>, @@ -449,40 +389,18 @@ pub async fn set_cloud_config_handler( if let Some(enable_sync) = update.enable_sync { manager - .cloud_services + .cloud_service .set_enable_sync(session.user_id, enable_sync); config.enable_sync = enable_sync; } - if let Some(enable_encrypt) = update.enable_encrypt { - debug_assert!(enable_encrypt, "Disable encryption is not supported"); - - if enable_encrypt { - tracing::info!("Enable encryption for user: {}", session.user_id); - config = config.with_enable_encrypt(enable_encrypt); - let encrypt_secret = config.encrypt_secret.clone(); - - // The encryption secret is generated when the user first enables encryption and will be - // used to validate the encryption secret is correct when the user logs in. - let encryption_sign = manager.generate_encryption_sign(session.user_id, &encrypt_secret)?; - let encryption_type = EncryptionType::SelfEncryption(encryption_sign); - manager - .set_encrypt_secret(session.user_id, encrypt_secret, encryption_type.clone()) - .await?; - - let params = - UpdateUserProfileParams::new(session.user_id).with_encryption_type(encryption_type); - manager.update_user_profile(params).await?; - } - } - save_cloud_config(session.user_id, &store_preferences, &config)?; let payload = CloudSettingPB { enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_services.service_url(), + server_url: manager.cloud_service.service_url(), }; send_notification( @@ -509,7 +427,7 @@ pub async fn get_cloud_config_handler( enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_services.service_url(), + server_url: manager.cloud_service.service_url(), }) } @@ -518,8 +436,10 @@ pub async fn get_all_workspace_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let uid = manager.get_session()?.user_id; - let user_workspaces = manager.get_all_user_workspaces(uid).await?; + let profile = manager.get_user_profile().await?; + let user_workspaces = manager + .get_all_user_workspaces(profile.uid, profile.auth_type) + .await?; data_result_ok(user_workspaces.into()) } @@ -542,7 +462,7 @@ pub async fn update_network_state_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let reachable = data.into_inner().ty.is_reachable(); - manager.cloud_services.set_network_reachable(reachable); + manager.cloud_service.set_network_reachable(reachable); manager .user_status_callback .read() @@ -687,8 +607,9 @@ pub async fn create_workspace_handler( manager: AFPluginState>, ) -> DataResult { let data = data.try_into_inner()?; + let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; - let new_workspace = manager.add_workspace(&data.name).await?; + let new_workspace = manager.create_workspace(&data.name, auth_type).await?; data_result_ok(new_workspace.into()) } @@ -712,9 +633,12 @@ pub async fn rename_workspace_handler( let params = rename_workspace_param.try_into_inner()?; let manager = upgrade_manager(manager)?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .patch_workspace(&workspace_id, Some(¶ms.new_name), None) - .await?; + let changeset = UserWorkspaceChangeset { + id: params.workspace_id, + name: Some(params.new_name), + icon: None, + }; + manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) } @@ -726,9 +650,12 @@ pub async fn change_workspace_icon_handler( let params = change_workspace_icon_param.try_into_inner()?; let manager = upgrade_manager(manager)?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .patch_workspace(&workspace_id, None, Some(¶ms.new_icon)) - .await?; + let changeset = UserWorkspaceChangeset { + id: workspace_id.to_string(), + name: None, + icon: Some(params.new_icon), + }; + manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) } @@ -825,9 +752,9 @@ pub async fn get_workspace_usage_handler( param: AFPluginData, manager: AFPluginState>, ) -> DataResult { - let workspace_id = param.into_inner().workspace_id; + let workspace_id = Uuid::from_str(¶m.into_inner().workspace_id)?; let manager = upgrade_manager(manager)?; - let workspace_usage = manager.get_workspace_usage(workspace_id).await?; + let workspace_usage = manager.get_workspace_usage(&workspace_id).await?; data_result_ok(WorkspaceUsagePB::from(workspace_usage)) } @@ -845,11 +772,12 @@ pub async fn update_workspace_subscription_payment_period_handler( params: AFPluginData, manager: AFPluginState>, ) -> FlowyResult<()> { + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let params = params.try_into_inner()?; let manager = upgrade_manager(manager)?; manager .update_workspace_subscription_payment_period( - params.workspace_id, + &workspace_id, params.plan.into(), params.recurring_interval.into(), ) @@ -884,7 +812,7 @@ pub async fn get_workspace_member_info( } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn update_workspace_setting( +pub async fn update_workspace_setting_handler( params: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { @@ -895,13 +823,14 @@ pub async fn update_workspace_setting( } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn get_workspace_setting( +pub async fn get_workspace_setting_handler( params: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let params = params.try_into_inner()?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let manager = upgrade_manager(manager)?; - let pb = manager.get_workspace_settings(¶ms.workspace_id).await?; + let pb = manager.get_workspace_settings(&workspace_id).await?; data_result_ok(pb) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 953011bc1c..03660ad0ff 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -35,8 +35,6 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetUserSetting, get_user_setting) .event(UserEvent::SetCloudConfig, set_cloud_config_handler) .event(UserEvent::GetCloudConfig, get_cloud_config_handler) - .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler) - .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler) .event(UserEvent::OauthSignIn, oauth_sign_in_handler) .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) @@ -77,8 +75,8 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::UpdateWorkspaceSubscriptionPaymentPeriod, update_workspace_subscription_payment_period_handler) .event(UserEvent::GetSubscriptionPlanDetails, get_subscription_plan_details_handler) // Workspace Setting - .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting) - .event(UserEvent::GetWorkspaceSetting, get_workspace_setting) + .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting_handler) + .event(UserEvent::GetWorkspaceSetting, get_workspace_setting_handler) .event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler) .event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler) } @@ -142,12 +140,6 @@ pub enum UserEvent { #[event(output = "CloudSettingPB")] GetCloudConfig = 14, - #[event(input = "UserSecretPB")] - SetEncryptionSecret = 15, - - #[event(output = "UserEncryptionConfigurationPB")] - CheckEncryptionSign = 16, - /// Return the all the workspaces of the user #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, @@ -257,7 +249,7 @@ pub enum UserEvent { #[event(input = "UpdateUserWorkspaceSettingPB")] UpdateWorkspaceSetting = 57, - #[event(input = "UserWorkspaceIdPB", output = "UseAISettingPB")] + #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSettingsPB")] GetWorkspaceSetting = 58, #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")] diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index a8bd91b55b..dd93593468 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -14,7 +14,7 @@ pub(crate) enum UserNotification { DidUpdateUserWorkspaces = 3, DidUpdateCloudConfig = 4, DidUpdateUserWorkspace = 5, - DidUpdateAISetting = 6, + DidUpdateWorkspaceSetting = 6, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index e324c2820f..f05c0bda95 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -7,20 +7,17 @@ use collab_plugins::local_storage::kv::KVTransactionDB; use dashmap::mapref::entry::Entry; use dashmap::DashMap; use flowy_error::FlowyError; -use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::ConnectionPool; use flowy_sqlite::{ query_dsl::*, schema::{user_table, user_table::dsl}, DBConnection, Database, ExpressionMethods, }; -use flowy_user_pub::entities::{UserProfile, UserWorkspace}; - +use flowy_user_pub::entities::UserProfile; use lib_infra::file_util::{unzip_and_replace, zip_folder}; use tracing::{error, event, info, instrument}; use crate::services::sqlite_sql::user_sql::UserTable; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; @@ -143,18 +140,6 @@ impl UserDB { Ok(user.into()) } - pub fn get_user_workspace( - &self, - pool: &Arc, - uid: i64, - ) -> Result, FlowyError> { - let mut conn = pool.get()?; - let row = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .first::(&mut *conn)?; - Ok(Some(UserWorkspace::from(row))) - } - /// Open a collab db for the user. If the db is already opened, return the opened db. /// fn open_collab_db( diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs index 93e642f72e..635c79ba39 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod member_sql; pub(crate) mod user_sql; +pub(crate) mod workspace_setting_sql; pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index 1138efd092..3fbd4c5854 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -1,6 +1,5 @@ use diesel::RunQueryDsl; use flowy_error::FlowyError; -use std::str::FromStr; use flowy_user_pub::cloud::UserUpdate; use flowy_user_pub::entities::*; @@ -15,40 +14,26 @@ use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; pub struct UserTable { pub(crate) id: String, pub(crate) name: String, - #[deprecated( - note = "The workspace_id is deprecated, please use the [Session::UserWorkspace] instead" - )] - pub(crate) workspace: String, pub(crate) icon_url: String, - pub(crate) openai_key: String, pub(crate) token: String, pub(crate) email: String, pub(crate) auth_type: i32, - pub(crate) encryption_type: String, - pub(crate) stability_ai_key: String, pub(crate) updated_at: i64, - pub(crate) ai_model: String, } #[allow(deprecated)] impl From<(UserProfile, AuthType)> for UserTable { fn from(value: (UserProfile, AuthType)) -> Self { let (user_profile, auth_type) = value; - let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTable { id: user_profile.uid.to_string(), name: user_profile.name, #[allow(deprecated)] - workspace: "".to_string(), icon_url: user_profile.icon_url, - openai_key: user_profile.openai_key, token: user_profile.token, email: user_profile.email, auth_type: auth_type as i32, - encryption_type, - stability_ai_key: user_profile.stability_ai_key, updated_at: user_profile.updated_at, - ai_model: user_profile.ai_model, } } } @@ -61,12 +46,8 @@ impl From for UserProfile { name: table.name, token: table.token, icon_url: table.icon_url, - openai_key: table.openai_key, - authenticator: AuthType::from(table.auth_type), - encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), - stability_ai_key: table.stability_ai_key, + auth_type: AuthType::from(table.auth_type), updated_at: table.updated_at, - ai_model: table.ai_model, } } } @@ -75,50 +56,30 @@ impl From for UserProfile { #[diesel(table_name = user_table)] pub struct UserTableChangeset { pub id: String, - pub workspace: Option, // deprecated pub name: Option, pub email: Option, pub icon_url: Option, - pub openai_key: Option, - pub encryption_type: Option, pub token: Option, - pub stability_ai_key: Option, - pub ai_model: Option, } impl UserTableChangeset { pub fn new(params: UpdateUserProfileParams) -> Self { - let encryption_type = params.encryption_sign.map(|sign| { - let ty = EncryptionType::from_sign(&sign); - serde_json::to_string(&ty).unwrap_or_default() - }); UserTableChangeset { id: params.uid.to_string(), - workspace: None, name: params.name, email: params.email, icon_url: params.icon_url, - openai_key: params.openai_key, - encryption_type, token: params.token, - stability_ai_key: params.stability_ai_key, - ai_model: params.ai_model, } } pub fn from_user_profile(user_profile: UserProfile) -> Self { - let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTableChangeset { id: user_profile.uid.to_string(), - workspace: None, name: Some(user_profile.name), email: Some(user_profile.email), icon_url: Some(user_profile.icon_url), - openai_key: Some(user_profile.openai_key), - encryption_type: Some(encryption_type), token: Some(user_profile.token), - stability_ai_key: Some(user_profile.stability_ai_key), - ai_model: Some(user_profile.ai_model), } } } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index 8d5c1e8dc7..f197ca9738 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -4,8 +4,7 @@ use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use flowy_user_pub::entities::UserWorkspace; -use std::convert::TryFrom; +use flowy_user_pub::entities::{AuthType, UserWorkspace}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] @@ -18,9 +17,45 @@ pub struct UserWorkspaceTable { pub icon: String, pub member_count: i64, pub role: Option, + pub auth_type: i32, } -pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option { +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceChangeset { + pub id: String, + pub name: Option, + pub icon: Option, +} + +impl UserWorkspaceTable { + pub fn from_workspace( + uid: i64, + workspace: &UserWorkspace, + auth_type: AuthType, + ) -> Result { + if workspace.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if workspace.workspace_database_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: workspace.id.clone(), + name: workspace.name.clone(), + uid, + created_at: workspace.created_at.timestamp(), + database_storage_id: workspace.workspace_database_id.clone(), + icon: workspace.icon.clone(), + member_count: workspace.member_count, + role: workspace.role.clone().map(|v| v as i32), + auth_type: auth_type as i32, + }) + } +} + +pub fn select_user_workspace(workspace_id: &str, mut conn: DBConnection) -> Option { user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) .first::(&mut *conn) @@ -28,7 +63,7 @@ pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Opti .map(UserWorkspace::from) } -pub fn get_all_user_workspace_op( +pub fn select_all_user_workspace( user_id: i64, mut conn: DBConnection, ) -> Result, FlowyError> { @@ -38,81 +73,45 @@ pub fn get_all_user_workspace_op( Ok(rows.into_iter().map(UserWorkspace::from).collect()) } -/// Remove all existing workspaces for given user and insert the new ones. -/// -#[allow(dead_code)] -pub fn save_user_workspaces_op( - uid: i64, +pub fn update_user_workspace( mut conn: DBConnection, - user_workspaces: &[UserWorkspace], + changeset: UserWorkspaceChangeset, ) -> Result<(), FlowyError> { - conn.immediate_transaction(|conn| { - delete_existing_workspaces(uid, conn)?; - insert_or_update_workspaces_op(uid, user_workspaces, conn)?; - Ok(()) - }) -} + diesel::update(user_workspace_table::dsl::user_workspace_table) + .filter(user_workspace_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(&mut conn)?; -#[allow(dead_code)] -fn delete_existing_workspaces(uid: i64, conn: &mut SqliteConnection) -> Result<(), FlowyError> { - diesel::delete( - user_workspace_table::dsl::user_workspace_table.filter(user_workspace_table::uid.eq(uid)), - ) - .execute(conn)?; Ok(()) } -pub fn insert_or_update_workspaces_op( +pub fn upsert_user_workspace( uid: i64, - user_workspaces: &[UserWorkspace], + auth_type: AuthType, + user_workspace: UserWorkspace, conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { - for user_workspace in user_workspaces { - let new_record = UserWorkspaceTable::try_from((uid, user_workspace))?; + let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - )) - .execute(conn)?; - } + diesel::insert_into(user_workspace_table::table) + .values(new_record.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(new_record.name), + user_workspace_table::uid.eq(new_record.uid), + user_workspace_table::created_at.eq(new_record.created_at), + user_workspace_table::database_storage_id.eq(new_record.database_storage_id), + user_workspace_table::icon.eq(new_record.icon), + user_workspace_table::member_count.eq(new_record.member_count), + user_workspace_table::role.eq(new_record.role), + user_workspace_table::auth_type.eq(new_record.auth_type), + )) + .execute(conn)?; Ok(()) } -impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { - type Error = FlowyError; - - fn try_from(value: (i64, &UserWorkspace)) -> Result { - if value.1.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if value.1.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: value.1.id.clone(), - name: value.1.name.clone(), - uid: value.0, - created_at: value.1.created_at.timestamp(), - database_storage_id: value.1.workspace_database_id.clone(), - icon: value.1.icon.clone(), - member_count: value.1.member_count, - role: value.1.role.clone().map(|v| v as i32), - }) - } -} - impl From for UserWorkspace { fn from(value: UserWorkspaceTable) -> Self { Self { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 8fb4009991..d7d7c8b68d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -39,17 +39,16 @@ use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; -use super::manager_user_workspace::save_user_workspace; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::user_manager::manager_user_encryption::validate_encryption_sign; +use crate::services::sqlite_sql::workspace_sql::upsert_user_workspace; use crate::user_manager::manager_user_workspace::save_all_user_workspaces; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; pub struct UserManager { - pub(crate) cloud_services: Arc, + pub(crate) cloud_service: Arc, pub(crate) store_preferences: Arc, pub(crate) user_awareness: Arc>>, pub(crate) user_status_callback: RwLock>, @@ -75,7 +74,7 @@ impl UserManager { let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { - cloud_services, + cloud_service: cloud_services, store_preferences, user_awareness: Default::default(), user_status_callback, @@ -89,7 +88,7 @@ impl UserManager { }); let weak_user_manager = Arc::downgrade(&user_manager); - if let Ok(user_service) = user_manager.cloud_services.get_user_service() { + if let Ok(user_service) = user_manager.cloud_service.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { tokio::spawn(async move { while let Some(update) = rx.recv().await { @@ -141,11 +140,11 @@ impl UserManager { // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.authenticator != AuthType::Local && user.authenticator != current_authenticator { + if user.auth_type != AuthType::Local && user.auth_type != current_authenticator { event!( tracing::Level::INFO, "Authenticator changed from {:?} to {:?}", - user.authenticator, + user.auth_type, current_authenticator ); self.sign_out().await?; @@ -157,7 +156,7 @@ impl UserManager { "init user session: {}:{}, authenticator: {:?}", user.uid, user.email, - user.authenticator, + user.auth_type, ); self.prepare_user(&session).await; @@ -166,21 +165,17 @@ impl UserManager { // Set the token if the current cloud service using token to authenticate // Currently, only the AppFlowy cloud using token to init the client api. // TODO(nathan): using trait to separate the init process for different cloud service - if user.authenticator.is_appflowy_cloud() { - if let Err(err) = self.cloud_services.set_token(&user.token) { + if user.auth_type.is_appflowy_cloud() { + if let Err(err) = self.cloud_service.set_token(&user.token) { error!("Set token failed: {}", err); } - if let Err(err) = self.cloud_services.set_ai_model(&user.ai_model) { - error!("Set ai model failed: {}", err); - } - // Subscribe the token state - let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_cloud_services = Arc::downgrade(&self.cloud_service); let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); let cloned_session = session.clone(); - if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { + if let Some(mut token_state_rx) = self.cloud_service.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; let local_token = user.token.clone(); @@ -273,18 +268,16 @@ impl UserManager { self.set_first_time_installed_version(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); // Init the user awareness. here we ignore the error - let _ = self - .initial_user_awareness(&session, &user.authenticator) - .await; + let _ = self.initial_user_awareness(&session, &user.auth_type).await; user_status_callback .did_init( user.uid, - &user.authenticator, + &user.auth_type, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, - &user.authenticator, + &user.auth_type, ) .await?; } else { @@ -349,12 +342,12 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - authenticator: AuthType, + auth_type: AuthType, ) -> Result { - self.cloud_services.set_server_auth_type(&authenticator); + self.cloud_service.set_server_auth_type(&auth_type); let response: AuthResponse = self - .cloud_services + .cloud_service .get_user_service()? .sign_in(BoxAny::new(params)) .await?; @@ -362,13 +355,11 @@ impl UserManager { self.prepare_user(&session).await; let latest_workspace = response.latest_workspace.clone(); - let user_profile = UserProfile::from((&response, &authenticator)); - self - .save_auth_data(&response, &authenticator, &session) - .await?; + let user_profile = UserProfile::from((&response, &auth_type)); + self.save_auth_data(&response, auth_type, &session).await?; let _ = self - .initial_user_awareness(&session, &user_profile.authenticator) + .initial_user_awareness(&session, &user_profile.auth_type) .await; self .user_status_callback @@ -378,7 +369,7 @@ impl UserManager { user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, - &authenticator, + &auth_type, ) .await?; send_auth_state_notification(AuthStateChangedPB { @@ -403,22 +394,13 @@ impl UserManager { ) -> Result { // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_services.set_server_auth_type(&auth_type); - let auth_service = self.cloud_services.get_user_service()?; + self.cloud_service.set_server_auth_type(&auth_type); + let auth_service = self.cloud_service.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); - if new_user_profile.encryption_type.require_encrypt_secret() { - self.auth_process.lock().await.replace(UserAuthProcess { - user_profile: new_user_profile.clone(), - migration_user, - response, - authenticator: auth_type, - }); - } else { - self - .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) - .await?; - } + self + .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) + .await?; Ok(new_user_profile) } @@ -455,10 +437,10 @@ impl UserManager { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, auth_type, &new_session) + .save_auth_data(&response, *auth_type, &new_session) .await?; let _ = self - .initial_user_awareness(&new_session, &new_user_profile.authenticator) + .initial_user_awareness(&new_session, &new_user_profile.auth_type) .await; self .user_status_callback @@ -514,7 +496,7 @@ impl UserManager { pub async fn sign_out(&self) -> Result<(), FlowyError> { if let Ok(session) = self.get_session() { sign_out( - &self.cloud_services, + &self.cloud_service, &session, &self.authenticate_user, self.db_connection(session.user_id)?, @@ -527,7 +509,7 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self))] pub async fn delete_account(&self) -> Result<(), FlowyError> { self - .cloud_services + .cloud_service .get_user_service()? .delete_account() .await?; @@ -553,10 +535,7 @@ impl UserManager { changeset, )?; - let profile = self.get_user_profile_from_disk(session.user_id).await?; - self - .update_user(session.user_id, profile.token, params) - .await?; + self.update_user(params).await?; Ok(()) } @@ -580,6 +559,12 @@ impl UserManager { .backup(session.user_id, &session.user_workspace.id); } + pub async fn get_user_profile(&self) -> FlowyResult { + let uid = self.get_session()?.user_id; + let profile = self.get_user_profile_from_disk(uid).await?; + Ok(profile) + } + /// Fetches the user profile for the given user ID. pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { select_user_profile(uid, self.db_connection(uid)?) @@ -588,7 +573,7 @@ impl UserManager { #[tracing::instrument(level = "info", skip_all, err)] pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile - if old_user_profile.authenticator.is_local() { + if old_user_profile.auth_type.is_local() { return Ok(()); } @@ -601,7 +586,7 @@ impl UserManager { let uid = old_user_profile.uid; let result: Result = self - .cloud_services + .cloud_service .get_user_service()? .get_user_profile(UserCredentials::from_uid(uid)) .await; @@ -610,7 +595,6 @@ impl UserManager { Ok(new_user_profile) => { // If the user profile is updated, save the new user profile if new_user_profile.updated_at > old_user_profile.updated_at { - validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); // Save the new user profile let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change( @@ -669,19 +653,11 @@ impl UserManager { Ok(None) } - async fn update_user( - &self, - uid: i64, - token: String, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { - let server = self.cloud_services.get_user_service()?; - tokio::spawn(async move { - let credentials = UserCredentials::new(Some(token), Some(uid), None); - server.update_user(credentials, params).await - }) - .await - .map_err(internal_error)??; + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + let server = self.cloud_service.get_user_service()?; + tokio::spawn(async move { server.update_user(params).await }) + .await + .map_err(internal_error)??; Ok(()) } @@ -702,7 +678,7 @@ impl UserManager { } pub async fn receive_realtime_event(&self, json: Value) { - if let Ok(user_service) = self.cloud_services.get_user_service() { + if let Ok(user_service) = self.cloud_service.get_user_service() { user_service.receive_realtime_event(json) } } @@ -712,9 +688,9 @@ impl UserManager { authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_server_auth_type(authenticator); + self.cloud_service.set_server_auth_type(authenticator); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; Ok(url) } @@ -725,9 +701,9 @@ impl UserManager { password: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) } @@ -738,9 +714,9 @@ impl UserManager { redirect_to: &str, ) -> Result<(), FlowyError> { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) .await?; @@ -753,9 +729,9 @@ impl UserManager { passcode: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) } @@ -765,9 +741,9 @@ impl UserManager { oauth_provider: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) .await?; @@ -778,27 +754,32 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - authenticator: &AuthType, + auth_type: AuthType, session: &Session, ) -> Result<(), FlowyError> { - let user_profile = UserProfile::from((response, authenticator)); + let user_profile = UserProfile::from((response, &auth_type)); let uid = user_profile.uid; - if authenticator.is_local() { + if auth_type.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); self.set_anon_user(session); } - save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; + save_all_user_workspaces( + uid, + self.db_connection(uid)?, + auth_type, + response.user_workspaces(), + )?; info!( "Save new user profile to disk, authenticator: {:?}", - authenticator + auth_type ); self .authenticate_user .set_session(Some(session.clone().into()))?; self - .save_user(uid, (user_profile, *authenticator).into()) + .save_user(uid, (user_profile, auth_type).into()) .await?; Ok(()) } @@ -807,11 +788,6 @@ impl UserManager { let session = self.get_session()?; if session.user_id == user_update.uid { debug!("Receive user update: {:?}", user_update); - let user_profile = self.get_user_profile_from_disk(user_update.uid).await?; - if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) { - return Ok(()); - } - // Save the user profile change upsert_user_profile_change( user_update.uid, @@ -827,27 +803,29 @@ impl UserManager { &self, old_user: &AnonUser, _new_user_session: &Session, - authenticator: &AuthType, + auth_type: &AuthType, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - if authenticator == &AuthType::AppFlowyCloud { + if auth_type == &AuthType::AppFlowyCloud { self .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) .await?; } // Save the old user workspace setting. - save_user_workspace( + let mut conn = self + .authenticate_user + .database + .get_connection(old_user.session.user_id)?; + upsert_user_workspace( old_user.session.user_id, - self - .authenticate_user - .database - .get_connection(old_user.session.user_id)?, - &old_user.session.user_workspace.clone(), + *auth_type, + old_user.session.user_workspace.clone(), + &mut conn, )?; Ok(()) } @@ -927,7 +905,7 @@ pub(crate) fn run_collab_data_migration( let migrations = collab_migration_list(); match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( migrations, - &user.authenticator, + &user.auth_type, app_version, ) { Ok(applied_migrations) => { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index ddb73667c8..7c5330149e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -24,7 +24,7 @@ impl UserManager { .await .ok()?; - if user_profile.authenticator.is_local() { + if user_profile.auth_type.is_local() { Some(AnonUser { session }) } else { None diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index 7de09995a0..afdfd218ef 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -215,7 +215,7 @@ impl UserManager { let collab_db = self.get_collab_db(session.user_id)?; let weak_builder = self.collab_builder.clone(); let user_awareness = Arc::downgrade(&self.user_awareness); - let cloud_services = self.cloud_services.clone(); + let cloud_services = self.cloud_service.clone(); let authenticate_user = self.authenticate_user.clone(); let is_loading_awareness = self.is_loading_awareness.clone(); @@ -376,7 +376,7 @@ impl UserManager { if !is_loading { let user_profile = self.get_user_profile_from_disk(session.user_id).await?; self - .initial_user_awareness(&session, &user_profile.authenticator) + .initial_user_awareness(&session, &user_profile.auth_type) .await?; } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs index 2bfba3422e..1462d1f019 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs @@ -1,31 +1,9 @@ -use crate::entities::{AuthStateChangedPB, AuthStatePB}; -use crate::notification::send_auth_state_notification; use crate::services::cloud_config::get_encrypt_secret; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{ - EncryptionType, UpdateUserProfileParams, UserCredentials, UserProfile, -}; use lib_infra::encryption::{decrypt_text, encrypt_text}; impl UserManager { - pub async fn set_encrypt_secret( - &self, - uid: i64, - secret: String, - encryption_type: EncryptionType, - ) -> FlowyResult<()> { - let params = UpdateUserProfileParams::new(uid).with_encryption_type(encryption_type); - self - .cloud_services - .get_user_service()? - .update_user(UserCredentials::from_uid(uid), params.clone()) - .await?; - self.cloud_services.set_encrypt_secret(secret); - - Ok(()) - } - pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult { let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?; Ok(encrypt_sign) @@ -63,16 +41,3 @@ impl UserManager { } } } - -pub(crate) fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { - // If the local user profile's encryption sign is not equal to the user update's encryption sign, - // which means the user enable encryption in another device, we should logout the current user. - let is_valid = user_profile.encryption_type.sign() == encryption_sign; - if !is_valid { - send_auth_state_notification(AuthStateChangedPB { - state: AuthStatePB::InvalidAuth, - message: "Encryption configuration was changed".to_string(), - }); - } - is_valid -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index b5ae54309b..3144b27213 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -2,7 +2,6 @@ use chrono::{Duration, NaiveDateTime, Utc}; use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlanDetail}; use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; -use std::convert::TryFrom; use std::str::FromStr; use std::sync::Arc; @@ -12,15 +11,14 @@ use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderD use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::entities::{ - Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, - WorkspaceMember, + AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use tracing::{error, info, instrument, trace, warn}; use uuid::Uuid; use crate::entities::{ RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, - UpdateUserWorkspaceSettingPB, UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB, + UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, }; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; @@ -31,12 +29,15 @@ use crate::services::data_import::{ use crate::services::sqlite_sql::member_sql::{ select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, }; -use crate::services::sqlite_sql::user_sql::UserTableChangeset; -use crate::services::sqlite_sql::workspace_sql::{ - get_all_user_workspace_op, get_user_workspace_op, insert_or_update_workspaces_op, - UserWorkspaceTable, +use crate::services::sqlite_sql::workspace_setting_sql::{ + select_workspace_setting, update_workspace_setting, upsert_workspace_setting, + WorkspaceSettingsChangeset, WorkspaceSettingsTable, }; -use crate::user_manager::{upsert_user_profile_change, UserManager}; +use crate::services::sqlite_sql::workspace_sql::{ + select_all_user_workspace, select_user_workspace, update_user_workspace, upsert_user_workspace, + UserWorkspaceChangeset, UserWorkspaceTable, +}; +use crate::user_manager::UserManager; use flowy_user_pub::session::Session; impl UserManager { @@ -119,12 +120,12 @@ impl UserManager { let user_id = current_session.user_id; let weak_user_collab_db = Arc::downgrade(&user_collab_db); - let weak_user_cloud_service = self.cloud_services.get_user_service()?; + let weak_user_cloud_service = self.cloud_service.get_user_service()?; match upload_collab_objects_data( user_id, weak_user_collab_db, ¤t_session.user_workspace.workspace_id()?, - &user.authenticator, + &user.auth_type, collab_data, weak_user_cloud_service, ) @@ -164,7 +165,7 @@ impl UserManager { pub async fn open_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("open workspace: {}", workspace_id); let user_workspace = self - .cloud_services + .cloud_service .get_user_service()? .open_workspace(workspace_id) .await?; @@ -177,7 +178,7 @@ impl UserManager { let user_profile = self.get_user_profile_from_disk(uid).await?; if let Err(err) = self - .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.authenticator) + .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.auth_type) .await { error!( @@ -190,7 +191,7 @@ impl UserManager { .user_status_callback .read() .await - .open_workspace(uid, &user_workspace, &user_profile.authenticator) + .open_workspace(uid, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); @@ -200,9 +201,13 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn add_workspace(&self, workspace_name: &str) -> FlowyResult { + pub async fn create_workspace( + &self, + workspace_name: &str, + auth_type: AuthType, + ) -> FlowyResult { let new_workspace = self - .cloud_services + .cloud_service .get_user_service()? .create_workspace(workspace_name) .await?; @@ -215,44 +220,29 @@ impl UserManager { // save the workspace to sqlite db let uid = self.user_id()?; let mut conn = self.db_connection(uid)?; - insert_or_update_workspaces_op(uid, &[new_workspace.clone()], &mut conn)?; + upsert_user_workspace(uid, auth_type, new_workspace.clone(), &mut conn)?; Ok(new_workspace) } pub async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + changeset: UserWorkspaceChangeset, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? - .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) + .patch_workspace(workspace_id, changeset.name.clone(), changeset.icon.clone()) .await?; // save the icon and name to sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - let mut user_workspace = match self.get_user_workspace(uid, workspace_id) { - Some(user_workspace) => user_workspace, - None => { - return Err(FlowyError::record_not_found().with_context(format!( - "Expected to find user workspace with id: {}, but not found", - workspace_id - ))); - }, - }; - - if let Some(new_workspace_name) = new_workspace_name { - user_workspace.name = new_workspace_name.to_string(); - } - if let Some(new_workspace_icon) = new_workspace_icon { - user_workspace.icon = new_workspace_icon.to_string(); - } - - let _ = save_user_workspace(uid, conn, &user_workspace); + update_user_workspace(conn, changeset)?; + let user_workspace = self + .get_user_workspace(uid, workspace_id) + .ok_or_else(FlowyError::record_not_found)?; let payload: UserWorkspacePB = user_workspace.clone().into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) @@ -265,7 +255,7 @@ impl UserManager { pub async fn leave_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("leave workspace: {}", workspace_id); self - .cloud_services + .cloud_service .get_user_service()? .leave_workspace(workspace_id) .await?; @@ -285,7 +275,7 @@ impl UserManager { pub async fn delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("delete workspace: {}", workspace_id); self - .cloud_services + .cloud_service .get_user_service()? .delete_workspace(workspace_id) .await?; @@ -308,7 +298,7 @@ impl UserManager { role: Role, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .invite_workspace_member(invitee_email, workspace_id, role) .await?; @@ -318,7 +308,7 @@ impl UserManager { pub async fn list_pending_workspace_invitations(&self) -> FlowyResult> { let status = Some(WorkspaceInvitationStatus::Pending); let invitations = self - .cloud_services + .cloud_service .get_user_service()? .list_workspace_invitations(status) .await?; @@ -327,7 +317,7 @@ impl UserManager { pub async fn accept_workspace_invitation(&self, invite_id: String) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .accept_workspace_invitations(invite_id) .await?; @@ -340,7 +330,7 @@ impl UserManager { workspace_id: Uuid, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .remove_workspace_member(user_email, workspace_id) .await?; @@ -352,7 +342,7 @@ impl UserManager { workspace_id: Uuid, ) -> FlowyResult> { let members = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_members(workspace_id) .await?; @@ -365,7 +355,7 @@ impl UserManager { uid: i64, ) -> FlowyResult { let member = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_member(workspace_id, uid) .await?; @@ -379,7 +369,7 @@ impl UserManager { role: Role, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .update_workspace_member(user_email, workspace_id, role) .await?; @@ -388,19 +378,23 @@ impl UserManager { pub fn get_user_workspace(&self, uid: i64, workspace_id: &Uuid) -> Option { let conn = self.db_connection(uid).ok()?; - get_user_workspace_op(workspace_id.to_string().as_str(), conn) + select_user_workspace(workspace_id.to_string().as_str(), conn) } - pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { + pub async fn get_all_user_workspaces( + &self, + uid: i64, + auth_type: AuthType, + ) -> FlowyResult> { let conn = self.db_connection(uid)?; - let workspaces = get_all_user_workspace_op(uid, conn)?; + let workspaces = select_all_user_workspace(uid, conn)?; - if let Ok(service) = self.cloud_services.get_user_service() { + if let Ok(service) = self.cloud_service.get_user_service() { if let Ok(pool) = self.db_pool(uid) { tokio::spawn(async move { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { - let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces); + let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) .payload(repeated_workspace_pbs) @@ -418,11 +412,12 @@ impl UserManager { &self, workspace_subscription: SubscribeWorkspacePB, ) -> FlowyResult { + let workspace_id = Uuid::from_str(&workspace_subscription.workspace_id)?; let payment_link = self - .cloud_services + .cloud_service .get_user_service()? .subscribe_workspace( - workspace_subscription.workspace_id, + workspace_id, workspace_subscription.recurring_interval.into(), workspace_subscription.workspace_subscription_plan.into(), workspace_subscription.success_url, @@ -437,10 +432,11 @@ impl UserManager { &self, workspace_id: String, ) -> FlowyResult { + let workspace_id = Uuid::from_str(&workspace_id)?; let subscriptions = self - .cloud_services + .cloud_service .get_user_service()? - .get_workspace_subscription_one(workspace_id.clone()) + .get_workspace_subscription_one(&workspace_id) .await?; Ok(WorkspaceSubscriptionInfoPB::from(subscriptions)) @@ -454,7 +450,7 @@ impl UserManager { reason: Option, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .cancel_workspace_subscription(workspace_id, plan, reason) .await?; @@ -464,12 +460,12 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .update_workspace_subscription_payment_period(workspace_id, plan, recurring_interval) .await?; @@ -479,7 +475,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_subscription_plan_details(&self) -> FlowyResult> { let plan_details = self - .cloud_services + .cloud_service .get_user_service()? .get_subscription_plan_details() .await?; @@ -489,10 +485,10 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> FlowyResult { let workspace_usage = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_usage(workspace_id) .await?; @@ -518,7 +514,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_billing_portal_url(&self) -> FlowyResult { let url = self - .cloud_services + .cloud_service .get_user_service()? .get_billing_portal_url() .await?; @@ -529,39 +525,92 @@ impl UserManager { &self, updated_settings: UpdateUserWorkspaceSettingPB, ) -> FlowyResult<()> { - let ai_model = updated_settings.ai_model.clone(); - let workspace_id = updated_settings.workspace_id.clone(); - let cloud_service = self.cloud_services.get_user_service()?; + let workspace_id = Uuid::from_str(&updated_settings.workspace_id)?; + let cloud_service = self.cloud_service.get_user_service()?; let settings = cloud_service - .update_workspace_setting(&workspace_id, updated_settings.into()) + .update_workspace_setting(&workspace_id, updated_settings.clone().into()) .await?; - let pb = UseAISettingPB::from(settings); + let changeset = WorkspaceSettingsChangeset { + id: workspace_id.to_string(), + disable_search_indexing: updated_settings.disable_search_indexing, + ai_model: updated_settings.ai_model.clone(), + }; + let uid = self.user_id()?; - send_notification(&uid.to_string(), UserNotification::DidUpdateAISetting) - .payload(pb) - .send(); + let mut conn = self.db_connection(uid)?; + update_workspace_setting(&mut conn, changeset)?; - if let Some(ai_model) = &ai_model { - if let Err(err) = self.cloud_services.set_ai_model(ai_model) { - error!("Set ai model failed: {}", err); - } - - let conn = self.db_connection(uid)?; - let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model); - upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; - } + let pb = WorkspaceSettingsPB::from(&settings); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(pb) + .send(); Ok(()) } - pub async fn get_workspace_settings(&self, workspace_id: &str) -> FlowyResult { - let cloud_service = self.cloud_services.get_user_service()?; - let settings = cloud_service.get_workspace_setting(workspace_id).await?; + pub async fn get_workspace_settings( + &self, + workspace_id: &Uuid, + ) -> FlowyResult { let uid = self.user_id()?; - let conn = self.db_connection(uid)?; - let params = UpdateUserProfileParams::new(uid).with_ai_model(&settings.ai_model); - upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; - Ok(UseAISettingPB::from(settings)) + let mut conn = self.db_connection(uid)?; + match select_workspace_setting(&mut conn, &workspace_id.to_string()) { + Ok(workspace_settings) => { + trace!("workspace settings found in local db"); + let pb = WorkspaceSettingsPB::from(workspace_settings); + + let old_pb = pb.clone(); + let workspace_id = *workspace_id; + let pool = self.db_pool(uid)?; + let cloud_service = self.cloud_service.clone(); + tokio::spawn(async move { + let cloud_service = cloud_service.get_user_service()?; + let settings = cloud_service.get_workspace_setting(&workspace_id).await?; + let new_pb = WorkspaceSettingsPB::from(&settings); + if new_pb != old_pb { + trace!("workspace settings updated"); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(new_pb) + .send(); + + if let Ok(mut conn) = pool.get() { + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + } + } + + Ok::<_, FlowyError>(()) + }); + Ok(pb) + }, + Err(err) => { + if err.is_record_not_found() { + trace!("No workspace settings found, fetch from remote"); + let settings = self + .cloud_service + .get_user_service()? + .get_workspace_setting(&workspace_id) + .await?; + let pb = WorkspaceSettingsPB::from(&settings); + let mut conn = self.db_connection(uid)?; + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + Ok::<_, FlowyError>(pb.clone()) + } else { + Err(err) + } + }, + } } pub async fn get_workspace_member_info( @@ -600,7 +649,7 @@ impl UserManager { ) -> FlowyResult { trace!("get workspace member info from remote: {}", workspace_id); let member = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_member_info(workspace_id, uid) .await?; @@ -629,7 +678,7 @@ impl UserManager { let plans = PeriodicallyCheckBillingState::new( workspace_id, success.plan.map(SubscriptionPlan::from), - Arc::downgrade(&self.cloud_services), + Arc::downgrade(&self.cloud_service), Arc::downgrade(&self.authenticate_user), ) .start() @@ -645,56 +694,21 @@ impl UserManager { } } -/// This method is used to save one user workspace to the SQLite database -/// -/// If the workspace is already persisted in the database, it will be overridden. -/// -/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user. -/// -pub fn save_user_workspace( - uid: i64, - mut conn: DBConnection, - user_workspace: &UserWorkspace, -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?; - let affected_rows = diesel::update( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(&user_workspace.id)), - ) - .set(( - user_workspace_table::name.eq(&user_workspace.name), - user_workspace_table::created_at.eq(&user_workspace.created_at), - user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), - user_workspace_table::icon.eq(&user_workspace.icon), - user_workspace_table::member_count.eq(&user_workspace.member_count), - )) - .execute(conn)?; - - if affected_rows == 0 { - diesel::insert_into(user_workspace_table::table) - .values(user_workspace) - .execute(conn)?; - } - - Ok::<(), FlowyError>(()) - }) -} - /// This method is used to save the user workspaces (plural) to the SQLite database /// /// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. /// -/// Consider using [save_user_workspace] if you only need to save a single workspace. +/// Consider using [upsert_user_workspace] if you only need to save a single workspace. /// pub fn save_all_user_workspaces( uid: i64, mut conn: DBConnection, + auth_type: AuthType, user_workspaces: &[UserWorkspace], ) -> FlowyResult<()> { let user_workspaces = user_workspaces .iter() - .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace))) + .map(|user_workspace| UserWorkspaceTable::from_workspace(uid, user_workspace, auth_type)) .collect::, _>>()?; conn.immediate_transaction(|conn| { From 3a05a4851fa83c8517e9f3ab29408a3c947975d6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:07:42 +0800 Subject: [PATCH 365/384] chore: clippy --- .../down.sql | 3 + .../up.sql | 24 ++++++ .../sqlite_sql/workspace_setting_sql.rs | 72 ++++++++++++++++ .../user_manager/manager_user_workspace.rs | 84 ++++++++++--------- 4 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql create mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql new file mode 100644 index 0000000000..50602eb129 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_workspace_table +DROP COLUMN auth_type; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql new file mode 100644 index 0000000000..b77372ad63 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql @@ -0,0 +1,24 @@ +-- Your SQL goes here +ALTER TABLE user_workspace_table + ADD COLUMN auth_type INTEGER NOT NULL DEFAULT 1; + +-- 2. Back‑fill from user_table.auth_type +UPDATE user_workspace_table +SET auth_type = (SELECT ut.auth_type + FROM user_table ut + WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) +WHERE EXISTS (SELECT 1 + FROM user_table ut + WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)); + +ALTER TABLE user_table DROP COLUMN stability_ai_key; +ALTER TABLE user_table DROP COLUMN openai_key; +ALTER TABLE user_table DROP COLUMN workspace; +ALTER TABLE user_table DROP COLUMN encryption_type; +ALTER TABLE user_table DROP COLUMN ai_model; + +CREATE TABLE workspace_setting_table ( + id TEXT PRIMARY KEY NOT NULL , + disable_search_indexing BOOLEAN DEFAULT FALSE NOT NULL , + ai_model TEXT DEFAULT "" NOT NULL +); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs new file mode 100644 index 0000000000..dcd87a36ee --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs @@ -0,0 +1,72 @@ +use client_api::entity::AFWorkspaceSettings; +use flowy_error::FlowyError; +use flowy_sqlite::schema::workspace_setting_table; +use flowy_sqlite::schema::workspace_setting_table::dsl; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use uuid::Uuid; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = workspace_setting_table)] +pub struct WorkspaceSettingsTable { + pub id: String, + pub disable_search_indexing: bool, + pub ai_model: String, +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = workspace_setting_table)] +pub struct WorkspaceSettingsChangeset { + pub id: String, + pub disable_search_indexing: Option, + pub ai_model: Option, +} + +impl WorkspaceSettingsTable { + pub fn from_workspace_settings(workspace_id: &Uuid, settings: &AFWorkspaceSettings) -> Self { + Self { + id: workspace_id.to_string(), + disable_search_indexing: settings.disable_search_indexing, + ai_model: settings.ai_model.clone(), + } + } +} + +pub fn update_workspace_setting( + conn: &mut DBConnection, + changeset: WorkspaceSettingsChangeset, +) -> Result<(), FlowyError> { + diesel::update(dsl::workspace_setting_table) + .filter(workspace_setting_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(conn)?; + Ok(()) +} + +/// Upserts a workspace setting into the database. +pub fn upsert_workspace_setting( + conn: &mut DBConnection, + settings: WorkspaceSettingsTable, +) -> Result<(), FlowyError> { + diesel::insert_into(dsl::workspace_setting_table) + .values(settings.clone()) + .on_conflict(workspace_setting_table::id) + .do_update() + .set(( + workspace_setting_table::disable_search_indexing.eq(settings.disable_search_indexing), + workspace_setting_table::ai_model.eq(settings.ai_model), + )) + .execute(conn)?; + Ok(()) +} + +/// Selects a workspace setting by id from the database. +pub fn select_workspace_setting( + conn: &mut DBConnection, + id: &str, +) -> Result { + let setting = dsl::workspace_setting_table + .filter(workspace_setting_table::id.eq(id)) + .first::(conn)?; + Ok(setting) +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 3144b27213..ea99a01f3a 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -5,17 +5,6 @@ use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; use std::str::FromStr; use std::sync::Arc; -use collab_integrate::CollabKVDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; -use flowy_user_pub::entities::{ - AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, -}; -use tracing::{error, info, instrument, trace, warn}; -use uuid::Uuid; - use crate::entities::{ RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, @@ -38,7 +27,18 @@ use crate::services::sqlite_sql::workspace_sql::{ UserWorkspaceChangeset, UserWorkspaceTable, }; use crate::user_manager::UserManager; +use collab_integrate::CollabKVDB; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_user_pub::cloud::UserCloudServiceProvider; +use flowy_user_pub::entities::{ + AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +}; use flowy_user_pub::session::Session; +use tracing::{error, info, instrument, trace, warn}; +use uuid::Uuid; impl UserManager { /// Import appflowy data from the given path. @@ -561,51 +561,29 @@ impl UserManager { Ok(workspace_settings) => { trace!("workspace settings found in local db"); let pb = WorkspaceSettingsPB::from(workspace_settings); - let old_pb = pb.clone(); let workspace_id = *workspace_id; + + // Spawn a task to sync remote settings using the helper let pool = self.db_pool(uid)?; let cloud_service = self.cloud_service.clone(); tokio::spawn(async move { - let cloud_service = cloud_service.get_user_service()?; - let settings = cloud_service.get_workspace_setting(&workspace_id).await?; - let new_pb = WorkspaceSettingsPB::from(&settings); - if new_pb != old_pb { - trace!("workspace settings updated"); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(new_pb) - .send(); - - if let Ok(mut conn) = pool.get() { - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), - )?; - } - } - - Ok::<_, FlowyError>(()) + let _ = sync_workspace_settings(cloud_service, workspace_id, old_pb, uid, pool).await; }); Ok(pb) }, Err(err) => { if err.is_record_not_found() { trace!("No workspace settings found, fetch from remote"); - let settings = self - .cloud_service - .get_user_service()? - .get_workspace_setting(&workspace_id) - .await?; + let service = self.cloud_service.get_user_service()?; + let settings = service.get_workspace_setting(&workspace_id).await?; let pb = WorkspaceSettingsPB::from(&settings); let mut conn = self.db_connection(uid)?; upsert_workspace_setting( &mut conn, WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), )?; - Ok::<_, FlowyError>(pb.clone()) + Ok(pb) } else { Err(err) } @@ -777,3 +755,31 @@ fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { None => false, } } + +async fn sync_workspace_settings( + cloud_service: Arc, + workspace_id: Uuid, + old_pb: WorkspaceSettingsPB, + uid: i64, + pool: Arc, +) -> FlowyResult<()> { + let service = cloud_service.get_user_service()?; + let settings = service.get_workspace_setting(&workspace_id).await?; + let new_pb = WorkspaceSettingsPB::from(&settings); + if new_pb != old_pb { + trace!("workspace settings updated"); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(new_pb) + .send(); + if let Ok(mut conn) = pool.get() { + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + } + } + Ok(()) +} From e851fba71b28c38b599ef1ef0a4d96339f97c349 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:21:22 +0800 Subject: [PATCH 366/384] chore: clippy --- .../flowy-user/src/user_manager/manager_user_workspace.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index ea99a01f3a..ac15324c00 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -576,12 +576,12 @@ impl UserManager { if err.is_record_not_found() { trace!("No workspace settings found, fetch from remote"); let service = self.cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(&workspace_id).await?; + let settings = service.get_workspace_setting(workspace_id).await?; let pb = WorkspaceSettingsPB::from(&settings); let mut conn = self.db_connection(uid)?; upsert_workspace_setting( &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + WorkspaceSettingsTable::from_workspace_settings(workspace_id, &settings), )?; Ok(pb) } else { From 84952b905694863bd4f300f47792a87f05371dee Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:24:32 +0800 Subject: [PATCH 367/384] chore: adjust generate theme script to import subtle colors (#7784) --- .../data/appflowy_default/primitive.dart | 362 +++++++++++++++++- .../theme/data/appflowy_default/semantic.dart | 2 +- .../appflowy_ui/script/generate_theme.dart | 211 ++++++---- 3 files changed, 509 insertions(+), 66 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart index 790db31660..2bd6d619d8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-16T22:13:33.297893 +// Generation time: 2025-04-19T13:45:56.076897 // // To modify these colors, edit the source JSON files and run the script: // @@ -295,4 +295,364 @@ class AppFlowyPrimitiveTokens { /// #7f6200 static Color get yellow1000 => Color(0xFF7F6200); + + /// #fcf2f2 + static Color get subtleColorRose100 => Color(0xFFFCF2F2); + + /// #fae3e3 + static Color get subtleColorRose200 => Color(0xFFFAE3E3); + + /// #fad9d9 + static Color get subtleColorRose300 => Color(0xFFFAD9D9); + + /// #edadad + static Color get subtleColorRose400 => Color(0xFFEDADAD); + + /// #cc4e4e + static Color get subtleColorRose500 => Color(0xFFCC4E4E); + + /// #702828 + static Color get subtleColorRose600 => Color(0xFF702828); + + /// #fcf4f0 + static Color get subtleColorPapaya100 => Color(0xFFFCF4F0); + + /// #fae8de + static Color get subtleColorPapaya200 => Color(0xFFFAE8DE); + + /// #fadfd2 + static Color get subtleColorPapaya300 => Color(0xFFFADFD2); + + /// #f0bda3 + static Color get subtleColorPapaya400 => Color(0xFFF0BDA3); + + /// #d67240 + static Color get subtleColorPapaya500 => Color(0xFFD67240); + + /// #6b3215 + static Color get subtleColorPapaya600 => Color(0xFF6B3215); + + /// #fff7ed + static Color get subtleColorTangerine100 => Color(0xFFFFF7ED); + + /// #fcedd9 + static Color get subtleColorTangerine200 => Color(0xFFFCEDD9); + + /// #fae5ca + static Color get subtleColorTangerine300 => Color(0xFFFAE5CA); + + /// #f2cb99 + static Color get subtleColorTangerine400 => Color(0xFFF2CB99); + + /// #db8f2c + static Color get subtleColorTangerine500 => Color(0xFFDB8F2C); + + /// #613b0a + static Color get subtleColorTangerine600 => Color(0xFF613B0A); + + /// #fff9ec + static Color get subtleColorMango100 => Color(0xFFFFF9EC); + + /// #fcf1d7 + static Color get subtleColorMango200 => Color(0xFFFCF1D7); + + /// #fae9c3 + static Color get subtleColorMango300 => Color(0xFFFAE9C3); + + /// #f5d68e + static Color get subtleColorMango400 => Color(0xFFF5D68E); + + /// #e0a416 + static Color get subtleColorMango500 => Color(0xFFE0A416); + + /// #5c4102 + static Color get subtleColorMango600 => Color(0xFF5C4102); + + /// #fffbe8 + static Color get subtleColorLemon100 => Color(0xFFFFFBE8); + + /// #fcf5cf + static Color get subtleColorLemon200 => Color(0xFFFCF5CF); + + /// #faefb9 + static Color get subtleColorLemon300 => Color(0xFFFAEFB9); + + /// #f5e282 + static Color get subtleColorLemon400 => Color(0xFFF5E282); + + /// #e0bb00 + static Color get subtleColorLemon500 => Color(0xFFE0BB00); + + /// #574800 + static Color get subtleColorLemon600 => Color(0xFF574800); + + /// #f9fae6 + static Color get subtleColorOlive100 => Color(0xFFF9FAE6); + + /// #f6f7d0 + static Color get subtleColorOlive200 => Color(0xFFF6F7D0); + + /// #f0f2b3 + static Color get subtleColorOlive300 => Color(0xFFF0F2B3); + + /// #dbde83 + static Color get subtleColorOlive400 => Color(0xFFDBDE83); + + /// #adb204 + static Color get subtleColorOlive500 => Color(0xFFADB204); + + /// #4a4c03 + static Color get subtleColorOlive600 => Color(0xFF4A4C03); + + /// #f6f9e6 + static Color get subtleColorLime100 => Color(0xFFF6F9E6); + + /// #eef5ce + static Color get subtleColorLime200 => Color(0xFFEEF5CE); + + /// #e7f0bb + static Color get subtleColorLime300 => Color(0xFFE7F0BB); + + /// #cfdb91 + static Color get subtleColorLime400 => Color(0xFFCFDB91); + + /// #92a822 + static Color get subtleColorLime500 => Color(0xFF92A822); + + /// #414d05 + static Color get subtleColorLime600 => Color(0xFF414D05); + + /// #f4faeb + static Color get subtleColorGrass100 => Color(0xFFF4FAEB); + + /// #e9f5d7 + static Color get subtleColorGrass200 => Color(0xFFE9F5D7); + + /// #def0c5 + static Color get subtleColorGrass300 => Color(0xFFDEF0C5); + + /// #bfd998 + static Color get subtleColorGrass400 => Color(0xFFBFD998); + + /// #75a828 + static Color get subtleColorGrass500 => Color(0xFF75A828); + + /// #334d0c + static Color get subtleColorGrass600 => Color(0xFF334D0C); + + /// #f1faf0 + static Color get subtleColorForest100 => Color(0xFFF1FAF0); + + /// #e2f5df + static Color get subtleColorForest200 => Color(0xFFE2F5DF); + + /// #d7f0d3 + static Color get subtleColorForest300 => Color(0xFFD7F0D3); + + /// #a8d6a1 + static Color get subtleColorForest400 => Color(0xFFA8D6A1); + + /// #49a33b + static Color get subtleColorForest500 => Color(0xFF49A33B); + + /// #1e4f16 + static Color get subtleColorForest600 => Color(0xFF1E4F16); + + /// #f0faf6 + static Color get subtleColorJade100 => Color(0xFFF0FAF6); + + /// #dff5eb + static Color get subtleColorJade200 => Color(0xFFDFF5EB); + + /// #cef0e1 + static Color get subtleColorJade300 => Color(0xFFCEF0E1); + + /// #90d1b5 + static Color get subtleColorJade400 => Color(0xFF90D1B5); + + /// #1c9963 + static Color get subtleColorJade500 => Color(0xFF1C9963); + + /// #075231 + static Color get subtleColorJade600 => Color(0xFF075231); + + /// #f0f9fa + static Color get subtleColorAqua100 => Color(0xFFF0F9FA); + + /// #dff3f5 + static Color get subtleColorAqua200 => Color(0xFFDFF3F5); + + /// #ccecf0 + static Color get subtleColorAqua300 => Color(0xFFCCECF0); + + /// #83ccd4 + static Color get subtleColorAqua400 => Color(0xFF83CCD4); + + /// #008e9e + static Color get subtleColorAqua500 => Color(0xFF008E9E); + + /// #004e57 + static Color get subtleColorAqua600 => Color(0xFF004E57); + + /// #f0f6fa + static Color get subtleColorAzure100 => Color(0xFFF0F6FA); + + /// #e1eef7 + static Color get subtleColorAzure200 => Color(0xFFE1EEF7); + + /// #d3e6f5 + static Color get subtleColorAzure300 => Color(0xFFD3E6F5); + + /// #88c0eb + static Color get subtleColorAzure400 => Color(0xFF88C0EB); + + /// #0877cc + static Color get subtleColorAzure500 => Color(0xFF0877CC); + + /// #154469 + static Color get subtleColorAzure600 => Color(0xFF154469); + + /// #f0f3fa + static Color get subtleColorDenim100 => Color(0xFFF0F3FA); + + /// #e3ebfa + static Color get subtleColorDenim200 => Color(0xFFE3EBFA); + + /// #d7e2f7 + static Color get subtleColorDenim300 => Color(0xFFD7E2F7); + + /// #9ab6ed + static Color get subtleColorDenim400 => Color(0xFF9AB6ED); + + /// #3267d1 + static Color get subtleColorDenim500 => Color(0xFF3267D1); + + /// #223c70 + static Color get subtleColorDenim600 => Color(0xFF223C70); + + /// #f2f2fc + static Color get subtleColorMauve100 => Color(0xFFF2F2FC); + + /// #e6e6fa + static Color get subtleColorMauve200 => Color(0xFFE6E6FA); + + /// #dcdcf7 + static Color get subtleColorMauve300 => Color(0xFFDCDCF7); + + /// #aeaef5 + static Color get subtleColorMauve400 => Color(0xFFAEAEF5); + + /// #5555e0 + static Color get subtleColorMauve500 => Color(0xFF5555E0); + + /// #36366b + static Color get subtleColorMauve600 => Color(0xFF36366B); + + /// #f6f3fc + static Color get subtleColorLavender100 => Color(0xFFF6F3FC); + + /// #ebe3fa + static Color get subtleColorLavender200 => Color(0xFFEBE3FA); + + /// #e4daf7 + static Color get subtleColorLavender300 => Color(0xFFE4DAF7); + + /// #c1aaf0 + static Color get subtleColorLavender400 => Color(0xFFC1AAF0); + + /// #8153db + static Color get subtleColorLavender500 => Color(0xFF8153DB); + + /// #462f75 + static Color get subtleColorLavender600 => Color(0xFF462F75); + + /// #f7f0fa + static Color get subtleColorLilac100 => Color(0xFFF7F0FA); + + /// #f0e1f7 + static Color get subtleColorLilac200 => Color(0xFFF0E1F7); + + /// #edd7f7 + static Color get subtleColorLilac300 => Color(0xFFEDD7F7); + + /// #d3a9e8 + static Color get subtleColorLilac400 => Color(0xFFD3A9E8); + + /// #9e4cc7 + static Color get subtleColorLilac500 => Color(0xFF9E4CC7); + + /// #562d6b + static Color get subtleColorLilac600 => Color(0xFF562D6B); + + /// #faf0fa + static Color get subtleColorMallow100 => Color(0xFFFAF0FA); + + /// #f5e1f4 + static Color get subtleColorMallow200 => Color(0xFFF5E1F4); + + /// #f5d7f4 + static Color get subtleColorMallow300 => Color(0xFFF5D7F4); + + /// #dea4dc + static Color get subtleColorMallow400 => Color(0xFFDEA4DC); + + /// #b240af + static Color get subtleColorMallow500 => Color(0xFFB240AF); + + /// #632861 + static Color get subtleColorMallow600 => Color(0xFF632861); + + /// #f9eff3 + static Color get subtleColorCamellia100 => Color(0xFFF9EFF3); + + /// #f7e1eb + static Color get subtleColorCamellia200 => Color(0xFFF7E1EB); + + /// #f7d7e5 + static Color get subtleColorCamellia300 => Color(0xFFF7D7E5); + + /// #e5a3c0 + static Color get subtleColorCamellia400 => Color(0xFFE5A3C0); + + /// #c24279 + static Color get subtleColorCamellia500 => Color(0xFFC24279); + + /// #6e2343 + static Color get subtleColorCamellia600 => Color(0xFF6E2343); + + /// #f5f5f5 + static Color get subtleColorSmoke100 => Color(0xFFF5F5F5); + + /// #e8e8e8 + static Color get subtleColorSmoke200 => Color(0xFFE8E8E8); + + /// #dedede + static Color get subtleColorSmoke300 => Color(0xFFDEDEDE); + + /// #b8b8b8 + static Color get subtleColorSmoke400 => Color(0xFFB8B8B8); + + /// #6e6e6e + static Color get subtleColorSmoke500 => Color(0xFF6E6E6E); + + /// #404040 + static Color get subtleColorSmoke600 => Color(0xFF404040); + + /// #f2f4f7 + static Color get subtleColorIron100 => Color(0xFFF2F4F7); + + /// #e6e9f0 + static Color get subtleColorIron200 => Color(0xFFE6E9F0); + + /// #dadee5 + static Color get subtleColorIron300 => Color(0xFFDADEE5); + + /// #b0b5bf + static Color get subtleColorIron400 => Color(0xFFB0B5BF); + + /// #666f80 + static Color get subtleColorIron500 => Color(0xFF666F80); + + /// #394152 + static Color get subtleColorIron600 => Color(0xFF394152); } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index 03e80da962..fe774d3561 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-16T22:13:33.307397 +// Generation time: 2025-04-19T13:45:56.089922 // // To modify these colors, edit the source JSON files and run the script: // diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart index 9d429f537e..bddcdb4eae 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -38,22 +38,13 @@ class AppFlowyPrimitiveTokens { // 3. Process each color category. jsonData.forEach((categoryName, categoryData) { - if (categoryData is Map) { - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map && - tokenData['\$type'] == 'color') { - final colorValue = tokenData['\$value'] as String; - final dartColorValue = convertColor(colorValue); - final dartTokenName = - '${categoryName}_$tokenName'.replaceAll('-', '_').toCamelCase(); - - buffer.writeln(''' - - /// $colorValue - static Color get $dartTokenName => Color(0x$dartColorValue);'''); - } - }); - } + categoryData.forEach((tokenName, tokenData) { + processPrimitiveTokenData( + buffer, + tokenData, + '${categoryName}_$tokenName', + ); + }); }); buffer.writeln('}'); @@ -65,6 +56,32 @@ class AppFlowyPrimitiveTokens { print('Successfully generated ${outputFile.path}'); } +void processPrimitiveTokenData( + StringBuffer buffer, + Map tokenData, + final String currentTokenName, +) { + if (tokenData + case { + r'$type': 'color', + r'$value': final String colorValue, + }) { + final dartColorValue = convertColor(colorValue); + final dartTokenName = currentTokenName.replaceAll('-', '_').toCamelCase(); + + buffer.writeln(''' + + /// $colorValue + static Color get $dartTokenName => Color(0x$dartColorValue);'''); + } else { + tokenData.forEach((key, value) { + if (value is Map) { + processPrimitiveTokenData(buffer, value, '${currentTokenName}_$key'); + } + }); + } +} + void generateSemantic() { // 1. Load the JSON file. final lightJsonString = @@ -98,61 +115,39 @@ import 'primitive.dart'; class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); // 3. Process light mode semantic tokens - void writeThemeData(String brightness, Map jsonData) { - buffer.writeln(''' + buffer.writeln(''' @override - AppFlowyThemeData $brightness() { + AppFlowyThemeData light() { final textStyle = AppFlowyBaseTextStyle(); final borderRadius = AppFlowySharedTokens.buildBorderRadius(); final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.$brightness);'''); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);'''); - jsonData.forEach((categoryName, categoryData) { - if (categoryData is Map) { - final hasNonColorType = categoryData.values.any( - (element) => - element is Map && element['\$type'] != 'color', - ); - if (hasNonColorType) { - return; - } + lightJsonData.forEach((categoryName, categoryData) { + if ([ + 'Spacing', + 'Border_Radius', + 'Shadow', + 'Badge_Color', + ].contains(categoryName)) { + return; + } - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map && - tokenData['\$type'] == 'color') { - final semanticTokenName = - tokenName.replaceAll('-', '_').toCamelCase(); - - final value = tokenData['\$value'] as String; - final String colorOrPrimitiveToken; - if (value.isColor) { - colorOrPrimitiveToken = 'Color(0x${convertColor(value)})'; - } else { - final primitiveToken = value - .replaceAll('{', '') - .replaceAll('}', '') - .replaceAll('.', '_') - .replaceAll('-', '_') - .toCamelCase(); - colorOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; - } - - buffer.writeln(' $semanticTokenName: $colorOrPrimitiveToken,'); - } - }); - buffer.writeln(' );'); - } + categoryData.forEach((tokenName, tokenData) { + processSemanticTokenData(buffer, tokenData, tokenName); }); + buffer.writeln(' );'); + }); - buffer.writeln(); - buffer.writeln(''' + buffer.writeln(); + buffer.writeln(''' return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, @@ -168,11 +163,61 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); shadow: shadow, ); }'''); - } - writeThemeData('light', lightJsonData); buffer.writeln(); - writeThemeData('dark', darkJsonData); + + buffer.writeln(''' + @override + AppFlowyThemeData dark() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);'''); + + darkJsonData.forEach((categoryName, categoryData) { + if ([ + 'Spacing', + 'Border_Radius', + 'Shadow', + 'Badge_Color', + ].contains(categoryName)) { + return; + } + + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); + + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map) { + processSemanticTokenData(buffer, tokenData, tokenName); + } + }); + buffer.writeln(' );'); + }); + + buffer.writeln(); + + buffer.writeln(''' + return AppFlowyThemeData( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + }'''); + buffer.writeln('}'); // 4. Write the output to a Dart file. @@ -182,6 +227,44 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); print('Successfully generated ${outputFile.path}'); } +void processSemanticTokenData( + StringBuffer buffer, + Map json, + final String currentTokenName, +) { + if (json + case { + r'$type': 'color', + r'$value': final String value, + }) { + final semanticTokenName = + currentTokenName.replaceAll('-', '_').toCamelCase(); + + final String colorValueOrPrimitiveToken; + if (value.isColor) { + colorValueOrPrimitiveToken = 'Color(0x${convertColor(value)})'; + } else { + final primitiveToken = value + .replaceAll(RegExp(r'\{|\}'), '') + .replaceAll(RegExp(r'\.|-'), '_') + .toCamelCase(); + colorValueOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; + } + + buffer.writeln(' $semanticTokenName: $colorValueOrPrimitiveToken,'); + } else { + json.forEach((key, value) { + if (value is Map) { + processSemanticTokenData( + buffer, + value, + '${currentTokenName}_$key', + ); + } + }); + } +} + String convertColor(String hexColor) { String color = hexColor.toUpperCase().replaceAll('#', ''); if (color.length == 6) { From 6dac45172e2e1e53ceacbcb82b4dea174f3022f3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 15:34:06 +0800 Subject: [PATCH 368/384] chore: auth type --- .../home/mobile_home_page_header.dart | 1 + .../lib/user/application/user_service.dart | 9 +- .../application/user/user_workspace_bloc.dart | 53 +++++++++--- .../workspace/_sidebar_workspace_menu.dart | 9 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++----- .../src/folder_event.rs | 16 +++- .../event-integration-test/src/user_event.rs | 13 +-- .../user/af_cloud_test/workspace_test.rs | 42 ++++++++-- .../flowy-core/src/user_state_callback.rs | 15 ++-- .../flowy-user/src/entities/user_profile.rs | 45 +++++++--- .../flowy-user/src/entities/workspace.rs | 32 ++++++- .../rust-lib/flowy-user/src/event_handler.rs | 29 ++++++- frontend/rust-lib/flowy-user/src/event_map.rs | 13 +-- .../src/services/sqlite_sql/workspace_sql.rs | 10 ++- .../flowy-user/src/user_manager/manager.rs | 1 - .../user_manager/manager_user_workspace.rs | 84 ++++++++++++++----- 16 files changed, 308 insertions(+), 110 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 97cc243c9e..4a1df63740 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -194,6 +194,7 @@ class _MobileWorkspace extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, + workspace.authType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 18f89ebc14..5e163b4e62 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -121,8 +121,13 @@ class UserBackendService implements IUserBackendService { }); } - Future> openWorkspace(String workspaceId) { - final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId; + Future> openWorkspace( + String workspaceId, + AuthTypePB authType, + ) { + final payload = OpenUserWorkspacePB() + ..workspaceId = workspaceId + ..authType = authType; return UserEventOpenWorkspace(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index e6a8c4d921..7e341f4f1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -52,7 +52,10 @@ class UserWorkspaceBloc extends Bloc { ); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); - await _userService.openWorkspace(currentWorkspace.workspaceId); + await _userService.openWorkspace( + currentWorkspace.workspaceId, + currentWorkspace.authType, + ); } emit( @@ -86,7 +89,12 @@ class UserWorkspaceBloc extends Bloc { Log.info( 'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}', ); - add(OpenWorkspace(currentWorkspace.workspaceId)); + add( + OpenWorkspace( + currentWorkspace.workspaceId, + currentWorkspace.authType, + ), + ); } }, createWorkspace: (name) async { @@ -118,7 +126,12 @@ class UserWorkspaceBloc extends Bloc { result ..onSuccess((s) { Log.info('create workspace success: $s'); - add(OpenWorkspace(s.workspaceId)); + add( + OpenWorkspace( + s.workspaceId, + s.authType, + ), + ); }) ..onFailure((f) { Log.error('create workspace error: $f'); @@ -171,7 +184,12 @@ class UserWorkspaceBloc extends Bloc { Log.info('delete workspace success: $workspaceId'); // if the current workspace is deleted, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }) ..onFailure((f) { @@ -179,7 +197,12 @@ class UserWorkspaceBloc extends Bloc { // if the workspace is deleted but return an error, we need to // open the first workspace if (!containsDeletedWorkspace) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }); emit( @@ -193,7 +216,7 @@ class UserWorkspaceBloc extends Bloc { ), ); }, - openWorkspace: (workspaceId) async { + openWorkspace: (workspaceId, authType) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -203,7 +226,10 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.openWorkspace(workspaceId); + final result = await _userService.openWorkspace( + workspaceId, + authType, + ); final currentWorkspace = result.fold( (s) => state.workspaces.firstWhereOrNull( (e) => e.workspaceId == workspaceId, @@ -337,7 +363,12 @@ class UserWorkspaceBloc extends Bloc { Log.info('leave workspace success: $workspaceId'); // if leaving the current workspace, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }) ..onFailure((f) { @@ -445,8 +476,10 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent { CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; - const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = - OpenWorkspace; + const factory UserWorkspaceEvent.openWorkspace( + String workspaceId, + AuthTypePB authType, + ) = OpenWorkspace; const factory UserWorkspaceEvent.renameWorkspace( String workspaceId, String name, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index ff393a8305..daf332cc15 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -306,9 +306,12 @@ class _WorkspaceInfo extends StatelessWidget { // Persist and close other tabs when switching workspace, restore tabs for new workspace getIt().add(TabsEvent.switchWorkspace(workspace.workspaceId)); - context - .read() - .add(UserWorkspaceEvent.openWorkspace(workspace.workspaceId)); + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + workspace.authType, + ), + ); PopoverContainer.of(context).closeAll(); } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 2e1b1cc417..345c1e58e0 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -11,8 +11,8 @@ use flowy_folder_pub::entities::PublishPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, - RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, WorkspaceMemberInvitationPB, - WorkspaceMemberPB, + RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, UserWorkspaceIdPB, UserWorkspacePB, + WorkspaceMemberInvitationPB, WorkspaceMemberPB, }; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; @@ -112,6 +112,18 @@ impl EventIntegrationTest { .parse::() } + pub async fn get_user_workspace(&self, workspace_id: &str) -> UserWorkspacePB { + let payload = UserWorkspaceIdPB { + workspace_id: workspace_id.to_string(), + }; + EventBuilder::new(self.clone()) + .event(UserEvent::GetUserWorkspace) + .payload(payload) + .async_send() + .await + .parse::() + } + pub fn get_folder_search_handler(&self) -> &Arc { self .appflowy_core diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 2ec74bafa6..5c9be65660 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,10 +17,10 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, - OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, - SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, - UserWorkspaceIdPB, UserWorkspacePB, + AuthTypePB, AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, + ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, + RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, + UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -280,9 +280,10 @@ impl EventIntegrationTest { .await; } - pub async fn open_workspace(&self, workspace_id: &str) { - let payload = UserWorkspaceIdPB { + pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) { + let payload = OpenUserWorkspacePB { workspace_id: workspace_id.to_string(), + auth_type, }; EventBuilder::new(self.clone()) .event(UserEvent::OpenWorkspace) diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index 366e43c157..ea04d922fc 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -89,7 +89,9 @@ async fn af_cloud_create_workspace_test() { } { // after opening new workspace - test.open_workspace(&created_workspace.workspace_id).await; + test + .open_workspace(&created_workspace.workspace_id, created_workspace.auth_type) + .await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); let views = test.folder_read_current_workspace_views().await; @@ -110,6 +112,7 @@ async fn af_cloud_open_workspace_test() { test.create_document("A").await; test.create_document("B").await; let first_workspace = test.get_current_workspace().await; + let first_workspace = test.get_user_workspace(&first_workspace.id).await; let views = test.get_all_workspace_views().await; assert_eq!(views.len(), 4); assert_eq!(views[0].name, default_document_name); @@ -120,8 +123,11 @@ async fn af_cloud_open_workspace_test() { let user_workspace = test .create_workspace("second workspace", AuthType::AppFlowyCloud) .await; - test.open_workspace(&user_workspace.workspace_id).await; + test + .open_workspace(&user_workspace.workspace_id, user_workspace.auth_type) + .await; let second_workspace = test.get_current_workspace().await; + let second_workspace = test.get_user_workspace(&second_workspace.id).await; test.create_document("C").await; test.create_document("D").await; @@ -135,13 +141,23 @@ async fn af_cloud_open_workspace_test() { // simulate open workspace and check if the views are correct for i in 0..10 { if i % 2 == 0 { - test.open_workspace(&first_workspace.id).await; + test + .open_workspace( + &first_workspace.workspace_id, + first_workspace.auth_type.clone(), + ) + .await; sleep(Duration::from_millis(300)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) .await; } else { - test.open_workspace(&second_workspace.id).await; + test + .open_workspace( + &second_workspace.workspace_id, + second_workspace.auth_type.clone(), + ) + .await; sleep(Duration::from_millis(200)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) @@ -149,14 +165,24 @@ async fn af_cloud_open_workspace_test() { } } - test.open_workspace(&first_workspace.id).await; + test + .open_workspace( + &first_workspace.workspace_id, + first_workspace.auth_type.clone(), + ) + .await; let views_1 = test.get_all_workspace_views().await; assert_eq!(views_1[0].name, default_document_name); assert_eq!(views_1[1].name, "Shared"); assert_eq!(views_1[2].name, "A"); assert_eq!(views_1[3].name, "B"); - test.open_workspace(&second_workspace.id).await; + test + .open_workspace( + &second_workspace.workspace_id, + second_workspace.auth_type.clone(), + ) + .await; let views_2 = test.get_all_workspace_views().await; assert_eq!(views_2[0].name, default_document_name); assert_eq!(views_2[1].name, "Shared"); @@ -212,7 +238,9 @@ async fn af_cloud_different_open_same_workspace_test() { for i in 0..30 { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; - client.open_workspace(iter_workspace_id).await; + client + .open_workspace(iter_workspace_id, all_workspaces[index].auth_type.clone()) + .await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; assert_eq!(views.len(), 2); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index ff1c6b6699..6002746603 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -49,11 +49,10 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn did_init( &self, user_id: i64, - auth_type: &AuthType, cloud_config: &Option, user_workspace: &UserWorkspace, _device_id: &str, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; self.server_provider.set_auth_type(*auth_type); @@ -81,7 +80,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator == &AuthType::Local) + .initialize(user_id, auth_type == &AuthType::Local) .await?; self.document_manager.initialize(user_id).await?; @@ -95,7 +94,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_id: i64, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { event!( tracing::Level::TRACE, @@ -103,6 +102,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); + self.server_provider.set_auth_type(*auth_type); self .folder_manager @@ -110,7 +110,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator.is_local()) + .initialize(user_id, auth_type.is_local()) .await?; self.document_manager.initialize(user_id).await?; @@ -207,15 +207,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { &self, user_id: i64, user_workspace: &UserWorkspace, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { + self.server_provider.set_auth_type(*auth_type); self .folder_manager .initialize_with_workspace_id(user_id) .await?; self .database_manager - .initialize(user_id, authenticator.is_local()) + .initialize(user_id, auth_type.is_local()) .await?; self.document_manager.initialize(user_id).await?; self.ai_manager.initialize(&user_workspace.id).await?; diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 54cb41db1e..f7a78a8f7d 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,15 +1,14 @@ +use super::AFRolePB; +use crate::entities::parser::{UserEmail, UserIcon, UserName}; +use crate::entities::{AuthTypePB, AuthenticatorPB}; +use crate::errors::ErrorCode; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::AuthenticatorPB; -use crate::errors::ErrorCode; - -use super::AFRolePB; - #[derive(Default, ProtoBuf)] pub struct UserTokenPB { #[pb(index = 1)] @@ -153,10 +152,14 @@ pub struct RepeatedUserWorkspacePB { pub items: Vec, } -impl From> for RepeatedUserWorkspacePB { - fn from(workspaces: Vec) -> Self { +impl From<(AuthType, Vec)> for RepeatedUserWorkspacePB { + fn from(value: (AuthType, Vec)) -> Self { + let (auth_type, workspaces) = value; Self { - items: workspaces.into_iter().map(UserWorkspacePB::from).collect(), + items: workspaces + .into_iter() + .map(|w| UserWorkspacePB::from((auth_type, w))) + .collect(), } } } @@ -181,17 +184,35 @@ pub struct UserWorkspacePB { #[pb(index = 6, one_of)] pub role: Option, + + #[pb(index = 7)] + pub auth_type: AuthTypePB, } -impl From for UserWorkspacePB { - fn from(value: UserWorkspace) -> Self { +impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { + fn from(value: (AuthType, UserWorkspace)) -> Self { + Self { + workspace_id: value.1.id, + name: value.1.name, + created_at_timestamp: value.1.created_at.timestamp(), + icon: value.1.icon, + member_count: value.1.member_count, + role: value.1.role.map(AFRolePB::from), + auth_type: AuthTypePB::from(value.0), + } + } +} + +impl From for UserWorkspacePB { + fn from(value: UserWorkspaceTable) -> Self { Self { workspace_id: value.id, name: value.name, - created_at_timestamp: value.created_at.timestamp(), + created_at_timestamp: value.created_at, icon: value.icon, member_count: value.member_count, role: value.role.map(AFRolePB::from), + auth_type: AuthTypePB::from(value.auth_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 26f848f3f5..d277fb4614 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -155,6 +155,17 @@ pub enum AFRolePB { Guest = 2, } +impl From for AFRolePB { + fn from(value: i32) -> Self { + match value { + 0 => AFRolePB::Owner, + 1 => AFRolePB::Member, + 2 => AFRolePB::Guest, + _ => AFRolePB::Guest, + } + } +} + impl From for Role { fn from(value: AFRolePB) -> Self { match value { @@ -182,6 +193,16 @@ pub struct UserWorkspaceIdPB { pub workspace_id: String, } +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct OpenUserWorkspacePB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub workspace_id: String, + + #[pb(index = 2)] + pub auth_type: AuthTypePB, +} + #[derive(ProtoBuf, Default, Clone, Validate)] pub struct CancelWorkspaceSubscriptionPB { #[pb(index = 1)] @@ -221,12 +242,21 @@ pub struct CreateWorkspacePB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Default, Clone)] +#[derive(ProtoBuf_Enum, Default, Debug, Clone)] pub enum AuthTypePB { LocalAuthType = 0, #[default] CloudAuthType = 1, } +impl From for AuthTypePB { + fn from(value: i32) -> Self { + match value { + 0 => AuthTypePB::LocalAuthType, + 1 => AuthTypePB::CloudAuthType, + _ => AuthTypePB::CloudAuthType, + } + } +} impl From for AuthTypePB { fn from(value: AuthType) -> Self { diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bb2a0b2c30..4cd49649ce 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -440,21 +440,42 @@ pub async fn get_all_workspace_handler( let user_workspaces = manager .get_all_user_workspaces(profile.uid, profile.auth_type) .await?; - data_result_ok(user_workspaces.into()) + + data_result_ok(RepeatedUserWorkspacePB::from(( + profile.auth_type, + user_workspaces, + ))) } #[tracing::instrument(level = "info", skip(data, manager), err)] pub async fn open_workspace_handler( - data: AFPluginData, + data: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager.open_workspace(&workspace_id).await?; + manager + .open_workspace(&workspace_id, AuthType::from(params.auth_type)) + .await?; Ok(()) } +#[tracing::instrument(level = "info", skip(data, manager), err)] +pub async fn get_user_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let params = data.try_into_inner()?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; + let uid = manager.user_id()?; + let user_workspace = manager + .get_user_workspace_from_db(uid, &workspace_id) + .ok_or_else(FlowyError::record_not_found)?; + data_result_ok(UserWorkspacePB::from(user_workspace)) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn update_network_state_handler( data: AFPluginData, @@ -610,7 +631,7 @@ pub async fn create_workspace_handler( let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; let new_workspace = manager.create_workspace(&data.name, auth_type).await?; - data_result_ok(new_workspace.into()) + data_result_ok(UserWorkspacePB::from((auth_type, new_workspace))) } #[tracing::instrument(level = "debug", skip_all, err)] diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 03660ad0ff..1ae414afa7 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -39,6 +39,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) + .event(UserEvent::GetUserWorkspace, get_user_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::OpenAnonUser, open_anon_user_handler) .event(UserEvent::GetAnonUser, get_anon_user_handler) @@ -144,9 +145,12 @@ pub enum UserEvent { #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, - #[event(input = "UserWorkspaceIdPB")] + #[event(input = "OpenUserWorkspacePB")] OpenWorkspace = 21, + #[event(input = "UserWorkspaceIdPB", output = "UserWorkspacePB")] + GetUserWorkspace = 22, + #[event(input = "NetworkStatePB")] UpdateNetworkState = 24, @@ -281,11 +285,10 @@ pub trait UserStatusCallback: Send + Sync + 'static { async fn did_init( &self, _user_id: i64, - _user_authenticator: &AuthType, _cloud_config: &Option, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -295,7 +298,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -318,7 +321,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { &self, _user_id: i64, _user_workspace: &UserWorkspace, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index f197ca9738..7fc3b00157 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -1,5 +1,5 @@ use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; +use diesel::RunQueryDsl; use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; @@ -55,12 +55,14 @@ impl UserWorkspaceTable { } } -pub fn select_user_workspace(workspace_id: &str, mut conn: DBConnection) -> Option { +pub fn select_user_workspace( + workspace_id: &str, + mut conn: DBConnection, +) -> Option { user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) .first::(&mut *conn) .ok() - .map(UserWorkspace::from) } pub fn select_all_user_workspace( @@ -89,7 +91,7 @@ pub fn upsert_user_workspace( uid: i64, auth_type: AuthType, user_workspace: UserWorkspace, - conn: &mut SqliteConnection, + conn: &mut DBConnection, ) -> Result<(), FlowyError> { let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index d7d7c8b68d..14aaebe86c 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -273,7 +273,6 @@ impl UserManager { user_status_callback .did_init( user.uid, - &user.auth_type, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index ac15324c00..b24764cc6b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -32,7 +32,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; -use flowy_user_pub::cloud::UserCloudServiceProvider; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; @@ -162,13 +162,32 @@ impl UserManager { } #[instrument(skip(self), err)] - pub async fn open_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { - info!("open workspace: {}", workspace_id); - let user_workspace = self - .cloud_service - .get_user_service()? - .open_workspace(workspace_id) - .await?; + pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { + info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + let uid = self.user_id()?; + let conn = self.db_connection(self.user_id()?)?; + let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { + None => { + sync_workspace( + workspace_id, + self.cloud_service.get_user_service()?, + uid, + auth_type, + self.db_pool(uid)?, + ) + .await? + }, + Some(row) => { + let user_workspace = UserWorkspace::from(row); + let workspace_id = *workspace_id; + let user_service = self.cloud_service.get_user_service()?; + let pool = self.db_pool(uid)?; + tokio::spawn(async move { + let _ = sync_workspace(&workspace_id, user_service, uid, auth_type, pool).await; + }); + user_workspace + }, + }; self .authenticate_user @@ -176,6 +195,15 @@ impl UserManager { let uid = self.user_id()?; let user_profile = self.get_user_profile_from_disk(uid).await?; + if let Err(err) = self + .user_status_callback + .read() + .await + .open_workspace(uid, &user_workspace, &user_profile.auth_type) + .await + { + error!("Open workspace failed: {:?}", err); + } if let Err(err) = self .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.auth_type) @@ -187,16 +215,6 @@ impl UserManager { ); } - if let Err(err) = self - .user_status_callback - .read() - .await - .open_workspace(uid, &user_workspace, &user_profile.auth_type) - .await - { - error!("Open workspace failed: {:?}", err); - } - Ok(()) } @@ -240,10 +258,11 @@ impl UserManager { let conn = self.db_connection(uid)?; update_user_workspace(conn, changeset)?; - let user_workspace = self - .get_user_workspace(uid, workspace_id) + let row = self + .get_user_workspace_from_db(uid, workspace_id) .ok_or_else(FlowyError::record_not_found)?; - let payload: UserWorkspacePB = user_workspace.clone().into(); + + let payload = UserWorkspacePB::from(row); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) .send(); @@ -376,7 +395,11 @@ impl UserManager { Ok(()) } - pub fn get_user_workspace(&self, uid: i64, workspace_id: &Uuid) -> Option { + pub fn get_user_workspace_from_db( + &self, + uid: i64, + workspace_id: &Uuid, + ) -> Option { let conn = self.db_connection(uid).ok()?; select_user_workspace(workspace_id.to_string().as_str(), conn) } @@ -395,7 +418,8 @@ impl UserManager { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); - let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); + let repeated_workspace_pbs = + RepeatedUserWorkspacePB::from((auth_type, new_user_workspaces)); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) .payload(repeated_workspace_pbs) .send(); @@ -783,3 +807,17 @@ async fn sync_workspace_settings( } Ok(()) } + +async fn sync_workspace( + workspace_id: &Uuid, + user_service: Arc, + uid: i64, + auth_type: AuthType, + pool: Arc, +) -> FlowyResult { + let user_workspace = user_service.open_workspace(workspace_id).await?; + if let Ok(mut conn) = pool.get() { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), &mut conn)?; + } + Ok(user_workspace) +} From 102087537a4a057170d3d040ac2dc98016fafee5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 15:50:42 +0800 Subject: [PATCH 369/384] chore: fmt --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 27ccdc8f18..28a83ed449 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); From 81f63bebe60fcf5a581ff6c49db71230d71e941f Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 21:03:50 +0800 Subject: [PATCH 370/384] chore: sql --- frontend/rust-lib/Cargo.lock | 5 +- .../src/local_server/impls/user.rs | 35 ++-- .../flowy-server/src/local_server/server.rs | 4 +- frontend/rust-lib/flowy-user-pub/Cargo.toml | 4 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 8 +- .../rust-lib/flowy-user-pub/src/entities.rs | 2 +- frontend/rust-lib/flowy-user-pub/src/lib.rs | 1 + .../rust-lib/flowy-user-pub/src/sql/mod.rs | 1 + .../flowy-user-pub/src/sql/workspace_sql.rs | 188 ++++++++++++++++++ frontend/rust-lib/flowy-user/Cargo.toml | 1 - .../src/services/sqlite_sql/workspace_sql.rs | 64 +++++- .../flowy-user/src/user_manager/manager.rs | 8 +- .../user_manager/manager_user_workspace.rs | 93 +-------- 13 files changed, 294 insertions(+), 120 deletions(-) create mode 100644 frontend/rust-lib/flowy-user-pub/src/sql/mod.rs create mode 100644 frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a703fb1e05..5beac0b3a6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -3086,7 +3086,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "nanoid", "protobuf", "quickcheck", "quickcheck_macros", @@ -3110,16 +3109,16 @@ dependencies = [ name = "flowy-user-pub" version = "0.1.0" dependencies = [ - "anyhow", - "arc-swap", "base64 0.21.5", "chrono", "client-api", "collab", "collab-entity", "collab-folder", + "diesel", "flowy-error", "flowy-folder-pub", + "flowy-sqlite", "lib-infra", "serde", "serde_json", diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 49db8e03a2..8189ec140c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,13 +1,17 @@ #![allow(unused_variables)] + use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; use lazy_static::lazy_static; +use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; +use crate::af_cloud::define::LoggedUser; +use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; @@ -16,14 +20,14 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use lib_infra::util::timestamp; -use crate::local_server::uid::UserIDGenerator; - lazy_static! { - //FIXME: seriously, userID generation should work using lock-free algorithm static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserServiceImpl; +pub(crate) struct LocalServerUserServiceImpl { + pub user: Arc, +} + #[async_trait] impl UserCloudService for LocalServerUserServiceImpl { async fn sign_up(&self, params: BoxAny) -> Result { @@ -128,7 +132,13 @@ impl UserCloudService for LocalServerUserServiceImpl { ) } - async fn get_all_workspace(&self, _uid: i64) -> Result, FlowyError> { + async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { + let conn = self.user.get_sqlite_db(uid)?; + + select_all_user_workspaces(&conn).await.map_err(|e| { + FlowyError::internal().with_context(format!("Failed to get all workspaces: {}", e)) + })?; + Ok(vec![]) } @@ -145,17 +155,11 @@ impl UserCloudService for LocalServerUserServiceImpl { new_workspace_name: Option, new_workspace_icon: Option, ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) + Ok(()) } async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) + Ok(()) } async fn get_user_awareness_doc_state( @@ -188,10 +192,7 @@ impl UserCloudService for LocalServerUserServiceImpl { workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support batch create collab object"), - ) + Ok(()) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index c81d526acb..8ce0d86221 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -41,7 +41,9 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { - Arc::new(LocalServerUserServiceImpl) + Arc::new(LocalServerUserServiceImpl { + user: self.user.clone(), + }) } fn folder_service(&self) -> Arc { diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index 80d087e88e..cb6b9b0738 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -15,7 +15,6 @@ collab-entity = { workspace = true } serde_json.workspace = true serde_repr.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } -anyhow.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.14" flowy-folder-pub.workspace = true @@ -23,4 +22,5 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } -arc-swap = "1.7.1" +flowy-sqlite.workspace = true +diesel.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dee44fa5f3..00eaef8917 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -293,7 +293,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_subscriptions( &self, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } /// Get the workspace subscriptions for a workspace @@ -301,7 +301,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn cancel_workspace_subscription( @@ -310,14 +310,14 @@ pub trait UserCloudService: Send + Sync + 'static { plan: SubscriptionPlan, reason: Option, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } async fn get_workspace_plan( &self, workspace_id: Uuid, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn get_workspace_usage( diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 7c8d775fa6..6be5a9f64d 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -164,7 +164,7 @@ impl UserWorkspace { workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), member_count: 1, - role: None, + role: Some(Role::Owner), } } } diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index 2e51ecc626..d820e8cd34 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,6 +1,7 @@ pub mod cloud; pub mod entities; pub mod session; +mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs new file mode 100644 index 0000000000..d5a7aaf317 --- /dev/null +++ b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs @@ -0,0 +1 @@ +pub mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs new file mode 100644 index 0000000000..ecd242c240 --- /dev/null +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -0,0 +1,188 @@ +use crate::entities::{AuthType, UserWorkspace}; +use chrono::{TimeZone, Utc}; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use tracing::{info, trace, warn}; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceTable { + pub id: String, + pub name: String, + pub uid: i64, + pub created_at: i64, + pub database_storage_id: String, + pub icon: String, + pub member_count: i64, + pub role: Option, + pub auth_type: i32, +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceChangeset { + pub id: String, + pub name: Option, + pub icon: Option, +} + +impl UserWorkspaceTable { + pub fn from_workspace( + uid: i64, + workspace: &UserWorkspace, + auth_type: AuthType, + ) -> Result { + if workspace.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if workspace.workspace_database_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: workspace.id.clone(), + name: workspace.name.clone(), + uid, + created_at: workspace.created_at.timestamp(), + database_storage_id: workspace.workspace_database_id.clone(), + icon: workspace.icon.clone(), + member_count: workspace.member_count, + role: workspace.role.clone().map(|v| v as i32), + auth_type: auth_type as i32, + }) + } +} + +pub fn select_user_workspace( + workspace_id: &str, + mut conn: DBConnection, +) -> Option { + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(workspace_id)) + .first::(&mut *conn) + .ok() +} + +pub fn select_all_user_workspace( + user_id: i64, + mut conn: DBConnection, +) -> Result, FlowyError> { + let rows = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(user_id)) + .load::(&mut *conn)?; + Ok(rows.into_iter().map(UserWorkspace::from).collect()) +} + +pub fn update_user_workspace( + mut conn: DBConnection, + changeset: UserWorkspaceChangeset, +) -> Result<(), FlowyError> { + diesel::update(user_workspace_table::dsl::user_workspace_table) + .filter(user_workspace_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(&mut conn)?; + + Ok(()) +} + +pub fn upsert_user_workspace( + uid: i64, + auth_type: AuthType, + user_workspace: UserWorkspace, + conn: &mut SqliteConnection, +) -> Result<(), FlowyError> { + let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; + diesel::insert_into(user_workspace_table::table) + .values(new_record.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(new_record.name), + user_workspace_table::uid.eq(new_record.uid), + user_workspace_table::created_at.eq(new_record.created_at), + user_workspace_table::database_storage_id.eq(new_record.database_storage_id), + user_workspace_table::icon.eq(new_record.icon), + user_workspace_table::member_count.eq(new_record.member_count), + user_workspace_table::role.eq(new_record.role), + user_workspace_table::auth_type.eq(new_record.auth_type), + )) + .execute(conn)?; + + Ok(()) +} + +pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::(rows_affected) + })?; + + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); + } + Ok(()) +} + +impl From for UserWorkspace { + fn from(value: UserWorkspaceTable) -> Self { + Self { + id: value.id, + name: value.name, + created_at: Utc + .timestamp_opt(value.created_at, 0) + .single() + .unwrap_or_default(), + workspace_database_id: value.database_storage_id, + icon: value.icon, + member_count: value.member_count, + role: value.role.map(|v| v.into()), + } + } +} + +/// Delete all user workspaces for the given user and auth type. +pub fn delete_user_all_workspace( + uid: i64, + auth_type: AuthType, + conn: &mut SqliteConnection, +) -> FlowyResult<()> { + let n = diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + ) + .execute(conn)?; + info!( + "Delete {} workspaces for user {} and auth type {:?}", + n, uid, auth_type + ); + Ok(()) +} + +/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. +pub fn delete_all_then_insert_user_workspaces( + uid: i64, + mut conn: DBConnection, + auth_type: AuthType, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + delete_user_all_workspace(uid, auth_type, conn)?; + + info!( + "Insert {} workspaces for user {} and auth type {:?}", + user_workspaces.len(), + uid, + auth_type + ); + for user_workspace in user_workspaces { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; + } + Ok::<(), FlowyError>(()) + }) +} diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 4d021161ac..65be4cc3f9 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -48,7 +48,6 @@ validator = { workspace = true, features = ["derive"] } rayon = "1.10.0" [dev-dependencies] -nanoid = "0.4.0" fake = "2.0.0" rand = "0.8.4" quickcheck = "1.0.3" diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index 7fc3b00157..46d5d74e35 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -1,10 +1,11 @@ use chrono::{TimeZone, Utc}; -use diesel::RunQueryDsl; -use flowy_error::FlowyError; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; use flowy_sqlite::{query_dsl::*, ExpressionMethods}; use flowy_user_pub::entities::{AuthType, UserWorkspace}; +use tracing::{info, trace, warn}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] @@ -91,10 +92,9 @@ pub fn upsert_user_workspace( uid: i64, auth_type: AuthType, user_workspace: UserWorkspace, - conn: &mut DBConnection, + conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) .values(new_record.clone()) .on_conflict(user_workspace_table::id) @@ -114,6 +114,20 @@ pub fn upsert_user_workspace( Ok(()) } +pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::(rows_affected) + })?; + + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); + } + Ok(()) +} + impl From for UserWorkspace { fn from(value: UserWorkspaceTable) -> Self { Self { @@ -130,3 +144,45 @@ impl From for UserWorkspace { } } } + +/// Delete all user workspaces for the given user and auth type. +pub fn delete_user_all_workspace( + uid: i64, + auth_type: AuthType, + conn: &mut SqliteConnection, +) -> FlowyResult<()> { + let n = diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + ) + .execute(conn)?; + info!( + "Delete {} workspaces for user {} and auth type {:?}", + n, uid, auth_type + ); + Ok(()) +} + +/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. +pub fn delete_all_then_insert_user_workspaces( + uid: i64, + mut conn: DBConnection, + auth_type: AuthType, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + delete_user_all_workspace(uid, auth_type, conn)?; + + info!( + "Insert {} workspaces for user {} and auth type {:?}", + user_workspaces.len(), + uid, + auth_type + ); + for user_workspace in user_workspaces { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; + } + Ok::<(), FlowyError>(()) + }) +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 14aaebe86c..8d75a2c150 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -41,8 +41,9 @@ use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::services::sqlite_sql::workspace_sql::upsert_user_workspace; -use crate::user_manager::manager_user_workspace::save_all_user_workspaces; +use crate::services::sqlite_sql::workspace_sql::{ + delete_all_then_insert_user_workspaces, upsert_user_workspace, +}; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; @@ -758,12 +759,13 @@ impl UserManager { ) -> Result<(), FlowyError> { let user_profile = UserProfile::from((response, &auth_type)); let uid = user_profile.uid; + if auth_type.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); self.set_anon_user(session); } - save_all_user_workspaces( + delete_all_then_insert_user_workspaces( uid, self.db_connection(uid)?, auth_type, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index b24764cc6b..1eaab813e4 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -23,21 +23,21 @@ use crate::services::sqlite_sql::workspace_setting_sql::{ WorkspaceSettingsChangeset, WorkspaceSettingsTable, }; use crate::services::sqlite_sql::workspace_sql::{ - select_all_user_workspace, select_user_workspace, update_user_workspace, upsert_user_workspace, - UserWorkspaceChangeset, UserWorkspaceTable, + delete_all_then_insert_user_workspaces, delete_user_workspace, select_all_user_workspace, + select_user_workspace, update_user_workspace, upsert_user_workspace, UserWorkspaceChangeset, + UserWorkspaceTable, }; use crate::user_manager::UserManager; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_sqlite::{ConnectionPool, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use flowy_user_pub::session::Session; -use tracing::{error, info, instrument, trace, warn}; +use tracing::{error, info, instrument, trace}; use uuid::Uuid; impl UserManager { @@ -282,7 +282,7 @@ impl UserManager { // delete workspace from local sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id.to_string().as_str())?; + delete_user_workspace(conn, workspace_id.to_string().as_str())?; self .user_workspace_service @@ -300,7 +300,7 @@ impl UserManager { .await?; let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id.to_string().as_str())?; + delete_user_workspace(conn, workspace_id.to_string().as_str())?; self .user_workspace_service @@ -417,7 +417,8 @@ impl UserManager { tokio::spawn(async move { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { - let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); + let _ = + delete_all_then_insert_user_workspaces(uid, conn, auth_type, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from((auth_type, new_user_workspaces)); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) @@ -696,82 +697,6 @@ impl UserManager { } } -/// This method is used to save the user workspaces (plural) to the SQLite database -/// -/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. -/// -/// Consider using [upsert_user_workspace] if you only need to save a single workspace. -/// -pub fn save_all_user_workspaces( - uid: i64, - mut conn: DBConnection, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], -) -> FlowyResult<()> { - let user_workspaces = user_workspaces - .iter() - .map(|user_workspace| UserWorkspaceTable::from_workspace(uid, user_workspace, auth_type)) - .collect::, _>>()?; - - conn.immediate_transaction(|conn| { - let existing_ids = user_workspace_table::dsl::user_workspace_table - .select(user_workspace_table::id) - .load::(conn)?; - let new_ids: Vec = user_workspaces.iter().map(|w| w.id.clone()).collect(); - let ids_to_delete: Vec = existing_ids - .into_iter() - .filter(|id| !new_ids.contains(id)) - .collect(); - - // insert or update the user workspaces - for user_workspace in &user_workspaces { - let affected_rows = diesel::update( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(&user_workspace.id)), - ) - .set(( - user_workspace_table::name.eq(&user_workspace.name), - user_workspace_table::created_at.eq(&user_workspace.created_at), - user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), - user_workspace_table::icon.eq(&user_workspace.icon), - user_workspace_table::member_count.eq(&user_workspace.member_count), - user_workspace_table::role.eq(&user_workspace.role), - )) - .execute(conn)?; - - if affected_rows == 0 { - diesel::insert_into(user_workspace_table::table) - .values(user_workspace) - .execute(conn)?; - } - } - - // delete the user workspaces that are not in the new list - if !ids_to_delete.is_empty() { - diesel::delete( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq_any(ids_to_delete)), - ) - .execute(conn)?; - } - - Ok::<(), FlowyError>(()) - }) -} - -pub fn delete_user_workspaces(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::(rows_affected) - })?; - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { let current_time: NaiveDateTime = Utc::now().naive_utc(); match current_time.checked_sub_signed(Duration::minutes(minutes)) { From 72fc0cce075fdd9183d1bc0376d77849ef8b1239 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 22:18:15 +0800 Subject: [PATCH 371/384] chore: move sql --- frontend/rust-lib/Cargo.lock | 1 - frontend/rust-lib/Cargo.toml | 1 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 18 ++ .../flowy-core/src/user_state_callback.rs | 34 +++- .../rust-lib/flowy-database2/src/manager.rs | 22 +- .../rust-lib/flowy-document/src/manager.rs | 15 +- frontend/rust-lib/flowy-folder/src/manager.rs | 8 +- .../src/local_server/impls/user.rs | 9 +- frontend/rust-lib/flowy-sqlite/Cargo.toml | 2 +- .../rust-lib/flowy-storage/src/manager.rs | 8 + frontend/rust-lib/flowy-user-pub/Cargo.toml | 3 +- frontend/rust-lib/flowy-user-pub/src/lib.rs | 2 +- .../src/sql}/member_sql.rs | 4 +- .../rust-lib/flowy-user-pub/src/sql/mod.rs | 10 +- .../src/sql}/user_sql.rs | 25 ++- .../src/sql}/workspace_setting_sql.rs | 2 +- .../flowy-user-pub/src/sql/workspace_sql.rs | 5 +- .../flowy-user/src/entities/user_profile.rs | 2 +- .../flowy-user/src/entities/workspace.rs | 2 +- .../rust-lib/flowy-user/src/event_handler.rs | 17 +- .../data_import/appflowy_data_import.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 19 +- .../rust-lib/flowy-user/src/services/mod.rs | 1 - .../flowy-user/src/services/sqlite_sql/mod.rs | 4 - .../src/services/sqlite_sql/workspace_sql.rs | 188 ------------------ .../flowy-user/src/user_manager/manager.rs | 19 +- .../user_manager/manager_user_workspace.rs | 16 +- 27 files changed, 148 insertions(+), 291 deletions(-) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/member_sql.rs (95%) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/user_sql.rs (81%) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/workspace_setting_sql.rs (97%) delete mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs delete mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 5beac0b3a6..e285ae83c8 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -3115,7 +3115,6 @@ dependencies = [ "collab", "collab-entity", "collab-folder", - "diesel", "flowy-error", "flowy-folder-pub", "flowy-sqlite", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ee99fcacf1..0112c862a8 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -77,6 +77,7 @@ diesel = { version = "2.1.0", features = [ "r2d2", "serde_json", ] } +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" futures = "0.3.31" diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 7602ff8040..399a8d2d5d 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -128,6 +128,24 @@ impl AIManager { Ok(()) } + #[instrument(skip_all, err)] + pub async fn initialize_after_open_workspace( + &self, + _workspace_id: &str, + ) -> Result<(), FlowyError> { + let local_ai = self.local_ai.clone(); + tokio::spawn(async move { + if let Err(err) = local_ai.destroy_plugin().await { + error!("Failed to destroy plugin: {}", err); + } + + if let Err(err) = local_ai.reload().await { + error!("[AI Manager] failed to reload local AI: {:?}", err); + } + }); + Ok(()) + } + pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { self.chats.entry(*chat_id).or_insert_with(|| { Arc::new(Chat::new( diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 6002746603..e85b773ec4 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -106,13 +106,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .folder_manager - .initialize_with_workspace_id(user_id) + .initialize_after_sign_in(user_id) .await?; self .database_manager - .initialize(user_id, auth_type.is_local()) + .initialize_after_sign_in(user_id, auth_type.is_local()) + .await?; + self + .document_manager + .initialize_after_sign_in(user_id) .await?; - self.document_manager.initialize(user_id).await?; let workspace_id = user_workspace.id.clone(); self.init_ai_component(workspace_id); @@ -171,7 +174,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .folder_manager - .initialize_with_new_user( + .initialize_after_sign_up( user_profile.uid, &user_profile.token, is_new_user, @@ -183,13 +186,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .database_manager - .initialize_with_new_user(user_profile.uid, auth_type.is_local()) + .initialize_after_sign_up(user_profile.uid, auth_type.is_local()) .await .context("DatabaseManager error")?; self .document_manager - .initialize_with_new_user(user_profile.uid) + .initialize_after_sign_up(user_profile.uid) .await .context("DocumentManager error")?; @@ -212,15 +215,24 @@ impl UserStatusCallback for UserStatusCallbackImpl { self.server_provider.set_auth_type(*auth_type); self .folder_manager - .initialize_with_workspace_id(user_id) + .initialize_after_open_workspace(user_id) .await?; self .database_manager - .initialize(user_id, auth_type.is_local()) + .initialize_after_open_workspace(user_id, auth_type.is_local()) .await?; - self.document_manager.initialize(user_id).await?; - self.ai_manager.initialize(&user_workspace.id).await?; - self.storage_manager.initialize(&user_workspace.id).await; + self + .document_manager + .initialize_after_open_workspace(user_id) + .await?; + self + .ai_manager + .initialize_after_open_workspace(&user_workspace.id) + .await?; + self + .storage_manager + .initialize_after_open_workspace(&user_workspace.id) + .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 49a946d108..6ae1e5cf15 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -134,12 +134,30 @@ impl DatabaseManager { } #[instrument( - name = "database_initialize_with_new_user", + name = "database_initialize_after_sign_up", level = "debug", skip_all, err )] - pub async fn initialize_with_new_user( + pub async fn initialize_after_sign_up( + &self, + user_id: i64, + is_local_user: bool, + ) -> FlowyResult<()> { + self.initialize(user_id, is_local_user).await?; + Ok(()) + } + + pub async fn initialize_after_open_workspace( + &self, + user_id: i64, + is_local_user: bool, + ) -> FlowyResult<()> { + self.initialize(user_id, is_local_user).await?; + Ok(()) + } + + pub async fn initialize_after_sign_in( &self, user_id: i64, is_local_user: bool, diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 704dcd0865..9c6a383bae 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -106,12 +106,23 @@ impl DocumentManager { } #[instrument( - name = "document_initialize_with_new_user", + name = "document_initialize_after_sign_up", level = "debug", skip_all, err )] - pub async fn initialize_with_new_user(&self, uid: i64) -> FlowyResult<()> { + pub async fn initialize_after_sign_up(&self, uid: i64) -> FlowyResult<()> { + self.initialize(uid).await?; + Ok(()) + } + + pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { + self.initialize(uid).await?; + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + pub async fn initialize_after_sign_in(&self, uid: i64) -> FlowyResult<()> { self.initialize(uid).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index e704be043d..8662c1e061 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -263,7 +263,7 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. #[tracing::instrument(skip(self, user_id), err)] - pub async fn initialize_with_workspace_id(&self, user_id: i64) -> FlowyResult<()> { + pub async fn initialize_after_sign_in(&self, user_id: i64) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; let object_id = &workspace_id; @@ -312,10 +312,14 @@ impl FolderManager { Ok(()) } + pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { + self.initialize_after_sign_in(uid).await + } + /// Initialize the folder for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user. #[instrument(level = "info", skip_all, err)] - pub async fn initialize_with_new_user( + pub async fn initialize_after_sign_up( &self, user_id: i64, _token: &str, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 8189ec140c..d378765ebb 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -15,6 +15,7 @@ use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::select_all_user_workspace; use flowy_user_pub::DEFAULT_USER_NAME; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -134,12 +135,8 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { let conn = self.user.get_sqlite_db(uid)?; - - select_all_user_workspaces(&conn).await.map_err(|e| { - FlowyError::internal().with_context(format!("Failed to get all workspaces: {}", e)) - })?; - - Ok(vec![]) + let workspaces = select_all_user_workspace(uid, conn)?; + Ok(workspaces) } async fn create_workspace(&self, _workspace_name: &str) -> Result { diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 0e85aebee5..345b05f903 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] diesel.workspace = true -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel_derives = { workspace = true, features = ["sqlite", "r2d2"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] } tracing.workspace = true serde.workspace = true diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index dc1bf053ea..619dc47f90 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -181,6 +181,14 @@ impl StorageManager { } } + pub async fn initialize_after_open_workspace(&self, workspace_id: &str) { + self.enable_storage_write_access(); + + if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { + error!("prepare {} upload task failed: {}", workspace_id, err); + } + } + pub fn update_network_reachable(&self, reachable: bool) { if reachable { self.uploader.resume(); diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index cb6b9b0738..f8a673e918 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -22,5 +22,4 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } -flowy-sqlite.workspace = true -diesel.workspace = true \ No newline at end of file +flowy-sqlite.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index d820e8cd34..773ae96a9a 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,7 +1,7 @@ pub mod cloud; pub mod entities; pub mod session; -mod sql; +pub mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs similarity index 95% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs index 70351ab105..bc73a8f34c 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs @@ -1,10 +1,8 @@ use diesel::{insert_into, RunQueryDsl}; use flowy_error::FlowyResult; - use flowy_sqlite::schema::workspace_members_table; - use flowy_sqlite::schema::workspace_members_table::dsl; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; #[derive(Queryable, Insertable, AsChangeset, Debug)] #[diesel(table_name = workspace_members_table)] diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs index d5a7aaf317..2a5f7bf891 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs @@ -1 +1,9 @@ -pub mod workspace_sql; +mod member_sql; +mod user_sql; +mod workspace_setting_sql; +mod workspace_sql; + +pub use member_sql::*; +pub use user_sql::*; +pub use workspace_setting_sql::*; +pub use workspace_sql::*; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs similarity index 81% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 3fbd4c5854..5a910888a8 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -1,12 +1,9 @@ -use diesel::RunQueryDsl; -use flowy_error::FlowyError; - -use flowy_user_pub::cloud::UserUpdate; -use flowy_user_pub::entities::*; - +use crate::cloud::UserUpdate; +use crate::entities::{AuthType, UpdateUserProfileParams, UserProfile}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; +use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; /// The order of the fields in the struct must be the same as the order of the fields in the table. /// Check out the [schema.rs] for table schema. #[derive(Clone, Default, Queryable, Identifiable, Insertable)] @@ -109,3 +106,17 @@ pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result FlowyResult<()> { + conn.immediate_transaction(|conn| { + // delete old user if exists + diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) + .execute(conn)?; + + let _ = diesel::insert_into(user_table::table) + .values(user) + .execute(conn)?; + Ok::<(), FlowyError>(()) + })?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs similarity index 97% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs index dcd87a36ee..667d1f0ca0 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs @@ -3,7 +3,7 @@ use flowy_error::FlowyError; use flowy_sqlite::schema::workspace_setting_table; use flowy_sqlite::schema::workspace_setting_table::dsl; use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use flowy_sqlite::{prelude::*, ExpressionMethods}; use uuid::Uuid; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index ecd242c240..bcac009527 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -1,11 +1,10 @@ use crate::entities::{AuthType, UserWorkspace}; use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use tracing::{info, trace, warn}; +use flowy_sqlite::{prelude::*, ExpressionMethods, RunQueryDsl, SqliteConnection}; +use tracing::{info, warn}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index f7a78a8f7d..17b951bae0 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -2,9 +2,9 @@ use super::AFRolePB; use crate::entities::parser::{UserEmail, UserIcon, UserName}; use crate::entities::{AuthTypePB, AuthenticatorPB}; use crate::errors::ErrorCode; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::UserWorkspaceTable; use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index d277fb4614..e178b724db 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -5,10 +5,10 @@ use client_api::entity::billing_dto::{ use serde::{Deserialize, Serialize}; use validator::Validate; -use crate::services::sqlite_sql::workspace_setting_sql::WorkspaceSettingsTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; +use flowy_user_pub::sql::WorkspaceSettingsTable; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 4cd49649ce..12dd1f3e93 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,6 +1,14 @@ +use crate::entities::*; +use crate::notification::{send_notification, UserNotification}; +use crate::services::cloud_config::{ + get_cloud_config, get_or_create_cloud_config, save_cloud_config, +}; +use crate::services::data_import::prepare_import; +use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::UserWorkspaceChangeset; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use serde_json::Value; @@ -10,15 +18,6 @@ use std::{convert::TryInto, sync::Arc}; use tracing::{event, trace}; use uuid::Uuid; -use crate::entities::*; -use crate::notification::{send_notification, UserNotification}; -use crate::services::cloud_config::{ - get_cloud_config, get_or_create_cloud_config, save_cloud_config, -}; -use crate::services::data_import::prepare_import; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceChangeset; -use crate::user_manager::UserManager; - fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { let manager = manager .upgrade() diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 106e911c1e..2db5f418de 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -3,7 +3,6 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::data_import::importer::load_collab_by_object_ids; use crate::services::db::UserDBPath; use crate::services::entities::UserPaths; -use crate::services::sqlite_sql::user_sql::select_user_profile; use crate::user_manager::run_collab_data_migration; use anyhow::anyhow; use collab::core::collab::DataSource; @@ -37,6 +36,7 @@ use std::collections::{HashMap, HashSet}; use collab_document::blocks::TextDelta; use collab_document::document::Document; +use flowy_user_pub::sql::select_user_profile; use semver::Version; use serde_json::json; use std::ops::{Deref, DerefMut}; diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index f05c0bda95..138ad95819 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -8,17 +8,12 @@ use dashmap::mapref::entry::Entry; use dashmap::DashMap; use flowy_error::FlowyError; use flowy_sqlite::ConnectionPool; -use flowy_sqlite::{ - query_dsl::*, - schema::{user_table, user_table::dsl}, - DBConnection, Database, ExpressionMethods, -}; +use flowy_sqlite::{DBConnection, Database}; use flowy_user_pub::entities::UserProfile; +use flowy_user_pub::sql::select_user_profile; use lib_infra::file_util::{unzip_and_replace, zip_folder}; use tracing::{error, event, info, instrument}; -use crate::services::sqlite_sql::user_sql::UserTable; - pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; fn collab_db_path(&self, uid: i64) -> PathBuf; @@ -131,13 +126,9 @@ impl UserDB { pool: &Arc, uid: i64, ) -> Result { - let uid = uid.to_string(); - let mut conn = pool.get()?; - let user = dsl::user_table - .filter(user_table::id.eq(&uid)) - .first::(&mut *conn)?; - - Ok(user.into()) + let conn = pool.get()?; + let profile = select_user_profile(uid, conn)?; + Ok(profile) } /// Open a collab db for the user. If the db is already opened, return the opened db. diff --git a/frontend/rust-lib/flowy-user/src/services/mod.rs b/frontend/rust-lib/flowy-user/src/services/mod.rs index 66316fa01a..ab4b3bea37 100644 --- a/frontend/rust-lib/flowy-user/src/services/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/mod.rs @@ -5,4 +5,3 @@ pub mod collab_interact; pub mod data_import; pub mod db; pub mod entities; -pub mod sqlite_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs deleted file mode 100644 index 635c79ba39..0000000000 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod member_sql; -pub(crate) mod user_sql; -pub(crate) mod workspace_setting_sql; -pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs deleted file mode 100644 index 46d5d74e35..0000000000 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ /dev/null @@ -1,188 +0,0 @@ -use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use flowy_user_pub::entities::{AuthType, UserWorkspace}; -use tracing::{info, trace, warn}; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceTable { - pub id: String, - pub name: String, - pub uid: i64, - pub created_at: i64, - pub database_storage_id: String, - pub icon: String, - pub member_count: i64, - pub role: Option, - pub auth_type: i32, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceChangeset { - pub id: String, - pub name: Option, - pub icon: Option, -} - -impl UserWorkspaceTable { - pub fn from_workspace( - uid: i64, - workspace: &UserWorkspace, - auth_type: AuthType, - ) -> Result { - if workspace.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if workspace.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: workspace.id.clone(), - name: workspace.name.clone(), - uid, - created_at: workspace.created_at.timestamp(), - database_storage_id: workspace.workspace_database_id.clone(), - icon: workspace.icon.clone(), - member_count: workspace.member_count, - role: workspace.role.clone().map(|v| v as i32), - auth_type: auth_type as i32, - }) - } -} - -pub fn select_user_workspace( - workspace_id: &str, - mut conn: DBConnection, -) -> Option { - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn) - .ok() -} - -pub fn select_all_user_workspace( - user_id: i64, - mut conn: DBConnection, -) -> Result, FlowyError> { - let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(user_id)) - .load::(&mut *conn)?; - Ok(rows.into_iter().map(UserWorkspace::from).collect()) -} - -pub fn update_user_workspace( - mut conn: DBConnection, - changeset: UserWorkspaceChangeset, -) -> Result<(), FlowyError> { - diesel::update(user_workspace_table::dsl::user_workspace_table) - .filter(user_workspace_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(&mut conn)?; - - Ok(()) -} - -pub fn upsert_user_workspace( - uid: i64, - auth_type: AuthType, - user_workspace: UserWorkspace, - conn: &mut SqliteConnection, -) -> Result<(), FlowyError> { - let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - user_workspace_table::auth_type.eq(new_record.auth_type), - )) - .execute(conn)?; - - Ok(()) -} - -pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::(rows_affected) - })?; - - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - -impl From for UserWorkspace { - fn from(value: UserWorkspaceTable) -> Self { - Self { - id: value.id, - name: value.name, - created_at: Utc - .timestamp_opt(value.created_at, 0) - .single() - .unwrap_or_default(), - workspace_database_id: value.database_storage_id, - icon: value.icon, - member_count: value.member_count, - role: value.role.map(|v| v.into()), - } - } -} - -/// Delete all user workspaces for the given user and auth type. -pub fn delete_user_all_workspace( - uid: i64, - auth_type: AuthType, - conn: &mut SqliteConnection, -) -> FlowyResult<()> { - let n = diesel::delete( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::auth_type.eq(auth_type as i32)), - ) - .execute(conn)?; - info!( - "Delete {} workspaces for user {} and auth type {:?}", - n, uid, auth_type - ); - Ok(()) -} - -/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. -pub fn delete_all_then_insert_user_workspaces( - uid: i64, - mut conn: DBConnection, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - delete_user_all_workspace(uid, auth_type, conn)?; - - info!( - "Insert {} workspaces for user {} and auth type {:?}", - user_workspaces.len(), - uid, - auth_type - ); - for user_workspace in user_workspaces { - upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; - } - Ok::<(), FlowyError>(()) - }) -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 8d75a2c150..86a3dfabce 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -40,13 +40,10 @@ use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; -use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::services::sqlite_sql::workspace_sql::{ - delete_all_then_insert_user_workspaces, upsert_user_workspace, -}; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; +use flowy_user_pub::sql::*; pub struct UserManager { pub(crate) cloud_service: Arc, @@ -662,18 +659,8 @@ impl UserManager { } async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { - let mut conn = self.db_connection(uid)?; - conn.immediate_transaction(|conn| { - // delete old user if exists - diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) - .execute(conn)?; - - let _ = diesel::insert_into(user_table::table) - .values(user) - .execute(conn)?; - Ok::<(), FlowyError>(()) - })?; - + let conn = self.db_connection(uid)?; + upsert_user(user, conn)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 1eaab813e4..cc884dc1d9 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -15,28 +15,18 @@ use crate::services::billing_check::PeriodicallyCheckBillingState; use crate::services::data_import::{ generate_import_data, upload_collab_objects_data, ImportedFolder, ImportedSource, }; -use crate::services::sqlite_sql::member_sql::{ - select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, -}; -use crate::services::sqlite_sql::workspace_setting_sql::{ - select_workspace_setting, update_workspace_setting, upsert_workspace_setting, - WorkspaceSettingsChangeset, WorkspaceSettingsTable, -}; -use crate::services::sqlite_sql::workspace_sql::{ - delete_all_then_insert_user_workspaces, delete_user_workspace, select_all_user_workspace, - select_user_workspace, update_user_workspace, upsert_user_workspace, UserWorkspaceChangeset, - UserWorkspaceTable, -}; + use crate::user_manager::UserManager; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::{ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_sqlite::ConnectionPool; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use flowy_user_pub::session::Session; +use flowy_user_pub::sql::*; use tracing::{error, info, instrument, trace}; use uuid::Uuid; From d478ecfd416fb8d0c5414eb92188beab71c4c3b2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 23:33:34 +0800 Subject: [PATCH 372/384] chore: remove unused code --- .../workspace_menu_bottom_sheet.dart | 1 + .../lib/startup/tasks/generate_router.dart | 12 - .../lib/user/application/user_service.dart | 20 +- .../helpers/handle_user_profile_result.dart | 21 -- .../user/presentation/helpers/helpers.dart | 1 - .../lib/user/presentation/router.dart | 4 - .../user/presentation/screens/screens.dart | 1 - .../presentation/screens/sign_up_screen.dart | 220 ------------------ .../application/user/user_workspace_bloc.dart | 13 +- .../application/workspace/workspace_bloc.dart | 4 +- .../workspace/_sidebar_workspace_menu.dart | 7 +- .../af_cloud/impls/user/cloud_service_impl.rs | 9 +- .../src/local_server/impls/user.rs | 34 +-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 6 +- .../rust-lib/flowy-user-pub/src/entities.rs | 34 --- .../flowy-user-pub/src/sql/workspace_sql.rs | 8 +- .../rust-lib/flowy-user/src/entities/auth.rs | 44 ---- .../rust-lib/flowy-user/src/event_handler.rs | 4 +- .../flowy-user/src/user_manager/manager.rs | 2 +- .../user_manager/manager_user_workspace.rs | 33 +-- 20 files changed, 69 insertions(+), 409 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index ef7f4492a5..eff2b4f420 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,6 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, + AuthTypePB.CloudAuthType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 7f9a2df329..e64e0f98de 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -119,18 +119,6 @@ GoRouter generateRouter(Widget child) { ); }, ), - GoRoute( - path: SignUpScreen.routeName, - pageBuilder: (context, state) { - return CustomTransitionPage( - child: SignUpScreen( - router: getIt(), - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 5e163b4e62..3ec181e009 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -140,25 +140,13 @@ class UserBackendService implements IUserBackendService { }); } - Future> createWorkspace( - String name, - String desc, - ) { - final request = CreateWorkspacePayloadPB.create() - ..name = name - ..desc = desc; - return FolderEventCreateFolderWorkspace(request).send().then((result) { - return result.fold( - (workspace) => FlowyResult.success(workspace), - (error) => FlowyResult.failure(error), - ); - }); - } - Future> createUserWorkspace( String name, + AuthTypePB authType, ) { - final request = CreateWorkspacePB.create()..name = name; + final request = CreateWorkspacePB.create() + ..name = name + ..authType = authType; return UserEventCreateWorkspace(request).send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart deleted file mode 100644 index 83007786f1..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy/user/presentation/helpers/helpers.dart'; -import 'package:appflowy/user/presentation/presentation.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/material.dart'; - -void handleUserProfileResult( - FlowyResult userProfileResult, - BuildContext context, - AuthRouter authRouter, -) { - userProfileResult.fold( - (userProfile) { - authRouter.goHomeScreen(context, userProfile); - }, - (error) { - handleOpenWorkspaceError(context, error); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart index 084a360666..11f321232e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart @@ -1,2 +1 @@ export 'handle_open_workspace_error.dart'; -export 'handle_user_profile_result.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index f6f6ec3e3a..339c2f29f7 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -21,10 +21,6 @@ class AuthRouter { getIt().pushWorkspaceStartScreen(context, userProfile); } - void pushSignUpScreen(BuildContext context) { - context.push(SignUpScreen.routeName); - } - /// Navigates to the home screen based on the current workspace and platform. /// /// This function takes in a [BuildContext] and a [UserProfilePB] object to diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 540da8c2b4..2aeba87995 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -1,6 +1,5 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; -export 'sign_up_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart deleted file mode 100644 index 8aea8dde55..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/sign_up_bloc.dart'; -import 'package:appflowy/user/presentation/router.dart'; -import 'package:appflowy/user/presentation/widgets/widgets.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SignUpScreen extends StatelessWidget { - const SignUpScreen({ - super.key, - required this.router, - }); - - static const routeName = '/SignUpScreen'; - final AuthRouter router; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), - child: BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null) { - _handleSuccessOrFail(context, successOrFail); - } - }, - child: const Scaffold(body: SignUpForm()), - ), - ); - } - - void _handleSuccessOrFail( - BuildContext context, - FlowyResult result, - ) { - result.fold( - (user) => router.pushWorkspaceStartScreen(context, user), - (error) => showSnapBar(context, error.msg), - ); - } -} - -class SignUpForm extends StatelessWidget { - const SignUpForm({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Align( - child: AuthFormContainer( - children: [ - FlowyLogoTitle( - title: LocaleKeys.signUp_title.tr(), - logoSize: const Size(60, 60), - ), - const VSpace(30), - const EmailTextField(), - const VSpace(5), - const PasswordTextField(), - const VSpace(5), - const RepeatPasswordTextField(), - const VSpace(30), - const SignUpButton(), - const VSpace(10), - const SignUpPrompt(), - if (context.read().state.isSubmitting) ...[ - const SizedBox(height: 8), - const LinearProgressIndicator(), - ], - ], - ), - ); - } -} - -class SignUpPrompt extends StatelessWidget { - const SignUpPrompt({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText.medium( - LocaleKeys.signUp_alreadyHaveAnAccount.tr(), - color: Theme.of(context).hintColor, - ), - TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.bodyMedium, - ), - onPressed: () => Navigator.pop(context), - child: FlowyText.medium( - LocaleKeys.signIn_buttonText.tr(), - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ); - } -} - -class SignUpButton extends StatelessWidget { - const SignUpButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return RoundedTextButton( - title: LocaleKeys.signUp_getStartedText.tr(), - height: 48, - onPressed: () { - context - .read() - .add(const SignUpEvent.signUpWithUserEmailAndPassword()); - }, - ); - } -} - -class PasswordTextField extends StatelessWidget { - const PasswordTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.passwordError != current.passwordError, - builder: (context, state) { - return RoundedInputField( - obscureText: true, - obscureIcon: const FlowySvg(FlowySvgs.hide_m), - obscureHideIcon: const FlowySvg(FlowySvgs.show_m), - hintText: LocaleKeys.signUp_passwordHint.tr(), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.passwordError ?? '', - onChanged: (value) => context - .read() - .add(SignUpEvent.passwordChanged(value)), - ); - }, - ); - } -} - -class RepeatPasswordTextField extends StatelessWidget { - const RepeatPasswordTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.repeatPasswordError != current.repeatPasswordError, - builder: (context, state) { - return RoundedInputField( - obscureText: true, - obscureIcon: const FlowySvg(FlowySvgs.hide_m), - obscureHideIcon: const FlowySvg(FlowySvgs.show_m), - hintText: LocaleKeys.signUp_repeatPasswordHint.tr(), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.repeatPasswordError ?? '', - onChanged: (value) => context - .read() - .add(SignUpEvent.repeatPasswordChanged(value)), - ); - }, - ); - } -} - -class EmailTextField extends StatelessWidget { - const EmailTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.emailError != current.emailError, - builder: (context, state) { - return RoundedInputField( - hintText: LocaleKeys.signUp_emailHint.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.emailError ?? '', - onChanged: (value) => - context.read().add(SignUpEvent.emailChanged(value)), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 7e341f4f1f..44e9fa3f92 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -97,7 +97,7 @@ class UserWorkspaceBloc extends Bloc { ); } }, - createWorkspace: (name) async { + createWorkspace: (name, authType) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -107,7 +107,10 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.createUserWorkspace(name); + final result = await _userService.createUserWorkspace( + name, + authType, + ); final workspaces = result.fold( (s) => [...state.workspaces, s], (e) => state.workspaces, @@ -472,8 +475,10 @@ class UserWorkspaceBloc extends Bloc { class UserWorkspaceEvent with _$UserWorkspaceEvent { const factory UserWorkspaceEvent.initial() = Initial; const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; - const factory UserWorkspaceEvent.createWorkspace(String name) = - CreateWorkspace; + const factory UserWorkspaceEvent.createWorkspace( + String name, + AuthTypePB authType, + ) = CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; const factory UserWorkspaceEvent.openWorkspace( diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index d8d5db45b4..0db1ac9443 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -2,6 +2,7 @@ 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-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -64,7 +65,8 @@ class WorkspaceBloc extends Bloc { String desc, Emitter emit, ) async { - final result = await userService.createWorkspace(name, desc); + final result = + await userService.createUserWorkspace(name, AuthTypePB.CloudAuthType); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index daf332cc15..24a122fee5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -386,7 +386,12 @@ class _CreateWorkspaceButton extends StatelessWidget { final workspaceBloc = context.read(); await CreateWorkspaceDialog( onConfirm: (name) { - workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); + workspaceBloc.add( + UserWorkspaceEvent.createWorkspace( + name, + AuthTypePB.CloudAuthType, + ), + ); }, ).show(context); } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 8309e4c65f..e0f81a62e4 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -24,8 +24,8 @@ use tracing::{instrument, trace}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; use flowy_user_pub::entities::{ - AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserCredentials, UserProfile, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserProfile, UserWorkspace, + WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -178,10 +178,7 @@ where } #[instrument(level = "debug", skip_all)] - async fn get_user_profile( - &self, - _credential: UserCredentials, - ) -> Result { + async fn get_user_profile(&self, _uid: i64) -> Result { let try_get_client = self.server.try_get_client(); let expected_workspace_id = self .logged_user diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index d378765ebb..eec50b1480 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -15,7 +15,7 @@ use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::select_all_user_workspace; +use flowy_user_pub::sql::{select_all_user_workspace, select_user_profile, select_user_workspace}; use flowy_user_pub::DEFAULT_USER_NAME; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -59,7 +59,7 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn sign_in(&self, params: BoxAny) -> Result { let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let user_workspace = make_user_workspace(); + let user_workspace = make_user_workspace("My Workspace"); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -122,15 +122,18 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } - async fn get_user_profile(&self, credential: UserCredentials) -> Result { - Err(FlowyError::local_version_not_support().with_context("Not support")) + async fn get_user_profile(&self, uid: i64) -> Result { + let conn = self.user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, conn)?; + Ok(profile) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support open workspace"), - ) + let uid = self.user.user_id()?; + let conn = self.user.get_sqlite_db(uid)?; + + let workspace = select_user_workspace(&workspace_id.to_string(), conn)?; + Ok(UserWorkspace::from(workspace)) } async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { @@ -139,11 +142,8 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(workspaces) } - async fn create_workspace(&self, _workspace_name: &str) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) + async fn create_workspace(&self, workspace_name: &str) -> Result { + Ok(make_user_workspace(workspace_name)) } async fn patch_workspace( @@ -193,12 +193,12 @@ impl UserCloudService for LocalServerUserServiceImpl { } } -fn make_user_workspace() -> UserWorkspace { +fn make_user_workspace(name: &str) -> UserWorkspace { UserWorkspace { - id: uuid::Uuid::new_v4().to_string(), - name: "My Workspace".to_string(), + id: Uuid::new_v4().to_string(), + name: name.to_string(), created_at: Default::default(), - workspace_database_id: uuid::Uuid::new_v4().to_string(), + workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), member_count: 1, role: None, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 00eaef8917..0964d80472 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -20,8 +20,8 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, AuthType, Role, UpdateUserProfileParams, UserCredentials, UserProfile, - UserTokenState, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, UserTokenState, + UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -168,7 +168,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Get the user information using the user's token or uid /// return None if the user is not found - async fn get_user_profile(&self, credential: UserCredentials) -> Result; + async fn get_user_profile(&self, uid: i64) -> Result; async fn open_workspace(&self, workspace_id: &Uuid) -> Result; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 6be5a9f64d..0d91f84b0e 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -100,40 +100,6 @@ impl UserAuthResponse for AuthResponse { } } -#[derive(Clone, Debug)] -pub struct UserCredentials { - /// Currently, the token is only used when the [AuthType] is AppFlowyCloud - pub token: Option, - - /// The user id - pub uid: Option, - - /// The user id - pub uuid: Option, -} - -impl UserCredentials { - pub fn from_uid(uid: i64) -> Self { - Self { - token: None, - uid: Some(uid), - uuid: None, - } - } - - pub fn from_uuid(uuid: String) -> Self { - Self { - token: None, - uid: None, - uuid: Some(uuid), - } - } - - pub fn new(token: Option, uid: Option, uuid: Option) -> Self { - Self { token, uid, uuid } - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserWorkspace { pub id: String, diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index bcac009527..a1165c9621 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -58,11 +58,11 @@ impl UserWorkspaceTable { pub fn select_user_workspace( workspace_id: &str, mut conn: DBConnection, -) -> Option { - user_workspace_table::dsl::user_workspace_table +) -> FlowyResult { + let row = user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn) - .ok() + .first::(&mut *conn)?; + Ok(row) } pub fn select_all_user_workspace( diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 6a889359c1..ddc0dc29f9 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -259,50 +259,6 @@ impl Default for AuthenticatorPB { } } -#[derive(Debug, ProtoBuf, Default)] -pub struct UserCredentialsPB { - #[pb(index = 1, one_of)] - pub uid: Option, - - #[pb(index = 2, one_of)] - pub uuid: Option, - - #[pb(index = 3, one_of)] - pub token: Option, -} - -impl UserCredentialsPB { - pub fn from_uid(uid: i64) -> Self { - Self { - uid: Some(uid), - uuid: None, - token: None, - } - } - - pub fn from_token(token: &str) -> Self { - Self { - uid: None, - uuid: None, - token: Some(token.to_owned()), - } - } - - pub fn from_uuid(uuid: &str) -> Self { - Self { - uid: None, - uuid: Some(uuid.to_owned()), - token: None, - } - } -} - -impl From for UserCredentials { - fn from(value: UserCredentialsPB) -> Self { - Self::new(value.token, value.uid, value.uuid) - } -} - #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 12dd1f3e93..bb38a9e0a0 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -469,9 +469,7 @@ pub async fn get_user_workspace_handler( let params = data.try_into_inner()?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let uid = manager.user_id()?; - let user_workspace = manager - .get_user_workspace_from_db(uid, &workspace_id) - .ok_or_else(FlowyError::record_not_found)?; + let user_workspace = manager.get_user_workspace_from_db(uid, &workspace_id)?; data_result_ok(UserWorkspacePB::from(user_workspace)) } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 86a3dfabce..6cfa8bf16f 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -585,7 +585,7 @@ impl UserManager { let result: Result = self .cloud_service .get_user_service()? - .get_user_profile(UserCredentials::from_uid(uid)) + .get_user_profile(uid) .await; match result { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index cc884dc1d9..17565e83df 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -157,17 +157,21 @@ impl UserManager { let uid = self.user_id()?; let conn = self.db_connection(self.user_id()?)?; let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { - None => { - sync_workspace( - workspace_id, - self.cloud_service.get_user_service()?, - uid, - auth_type, - self.db_pool(uid)?, - ) - .await? + Err(err) => { + if err.is_record_not_found() { + sync_workspace( + workspace_id, + self.cloud_service.get_user_service()?, + uid, + auth_type, + self.db_pool(uid)?, + ) + .await? + } else { + return Err(err); + } }, - Some(row) => { + Ok(row) => { let user_workspace = UserWorkspace::from(row); let workspace_id = *workspace_id; let user_service = self.cloud_service.get_user_service()?; @@ -248,10 +252,7 @@ impl UserManager { let conn = self.db_connection(uid)?; update_user_workspace(conn, changeset)?; - let row = self - .get_user_workspace_from_db(uid, workspace_id) - .ok_or_else(FlowyError::record_not_found)?; - + let row = self.get_user_workspace_from_db(uid, workspace_id)?; let payload = UserWorkspacePB::from(row); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) @@ -389,8 +390,8 @@ impl UserManager { &self, uid: i64, workspace_id: &Uuid, - ) -> Option { - let conn = self.db_connection(uid).ok()?; + ) -> FlowyResult { + let conn = self.db_connection(uid)?; select_user_workspace(workspace_id.to_string().as_str(), conn) } From 2f5b494885904aac89cd173d0eb0978c7a0bd9b7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 00:28:15 +0800 Subject: [PATCH 373/384] chore: remove authticator pb --- .../bottom_sheet/bottom_sheet_view_page.dart | 3 +- .../home/setting/settings_popup_menu.dart | 2 +- .../home/tab/mobile_space_tab.dart | 2 +- .../workspace_menu_bottom_sheet.dart | 2 +- .../setting/user_session_setting_group.dart | 2 +- .../lib/plugins/ai_chat/chat_page.dart | 2 +- .../application/sync/database_sync_bloc.dart | 2 +- .../database/widgets/row/row_banner.dart | 5 +- .../document/application/document_bloc.dart | 4 +- .../document_collaborators_bloc.dart | 3 +- .../application/document_sync_bloc.dart | 3 +- .../editor_plugins/file/file_util.dart | 6 +-- .../page_style/_page_style_cover_image.dart | 3 +- .../lib/plugins/shared/share/share_bloc.dart | 2 +- .../icon_emoji_picker/icon_uploader.dart | 6 +-- .../lib/startup/deps_resolver.dart | 2 +- .../startup/tasks/appflowy_cloud_task.dart | 2 +- .../auth/af_cloud_auth_service.dart | 2 +- .../auth/af_cloud_mock_auth_service.dart | 6 +-- .../auth/backend_auth_service.dart | 7 +-- .../application/password/password_bloc.dart | 4 +- .../lib/user/presentation/anon_user.dart | 2 +- .../settings/settings_dialog_bloc.dart | 4 +- .../application/user/user_workspace_bloc.dart | 2 +- .../application/workspace/workspace_bloc.dart | 2 +- .../workspace/_sidebar_workspace_menu.dart | 2 +- .../menu/view/view_more_action_button.dart | 2 +- .../settings/pages/settings_account_view.dart | 28 +++++------ .../pages/settings_workspace_view.dart | 4 +- .../settings/settings_dialog.dart | 2 +- .../settings/widgets/settings_menu.dart | 4 +- .../more_view_actions/more_view_actions.dart | 2 +- .../event-integration-test/src/lib.rs | 4 +- .../event-integration-test/src/user_event.rs | 10 ++-- .../user/af_cloud_test/anon_user_test.rs | 4 +- .../tests/user/local_test/auth_test.rs | 8 ++-- .../user/local_test/user_profile_test.rs | 4 +- .../src/local_server/impls/user.rs | 24 ++++------ .../rust-lib/flowy-user-pub/src/entities.rs | 6 +-- .../flowy-user-pub/src/sql/workspace_sql.rs | 20 ++++---- .../rust-lib/flowy-user/src/entities/auth.rs | 47 ++++--------------- .../flowy-user/src/entities/user_profile.rs | 4 +- .../flowy-user/src/entities/workspace.rs | 25 ++++++---- .../user_manager/manager_user_workspace.rs | 22 ++++++--- 44 files changed, 134 insertions(+), 168 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 85a5e3cbfa..9706777df0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -202,8 +202,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { List _buildPublishActions(BuildContext context) { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud - if (userProfile == null || - userProfile.authType != AuthenticatorPB.AppFlowyCloud) { + if (userProfile == null || userProfile.authType != AuthTypePB.Server) { return []; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index 11a82d2c7a..bd41730934 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthTypePB.Server) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index cc3f4c8c56..c89367f379 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -167,7 +167,7 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthTypePB.Server) Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 20, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index eff2b4f420..d306f48964 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.CloudAuthType, + AuthTypePB.Server, ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index 09f38223f4..405fef0d1a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget { // delete account button // only show the delete account button in cloud mode - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthTypePB.Server) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index c154b0e3a0..90085354db 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -48,7 +48,7 @@ class AIChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - // if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + // if (userProfile.authenticator != AuthTypePB.Server) { // return Center( // child: FlowyText( // LocaleKeys.chat_unsupportedCloudPrompt.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index 53dc2bd6d5..5116785c1f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud && + userProfile?.authType == AuthTypePB.Server && databaseId != null, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 5706167f1a..e2f470e0d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -21,8 +21,8 @@ import 'package:appflowy/shared/af_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -69,8 +69,7 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.authType ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + (widget.userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; @override void dispose() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 252742aa5e..ac03fe5308 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -101,8 +101,8 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.authType ?? AuthenticatorPB.Local; - return type == AuthenticatorPB.Local; + final type = userProfilePB?.authType ?? AuthTypePB.Local; + return type == AuthTypePB.Local; } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart index b593ccc6cc..682f600f0a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -31,8 +31,7 @@ class DocumentCollaboratorsBloc final userProfile = result.fold((s) => s, (f) => null); emit( state.copyWith( - shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud, + shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 1001aaef5e..2ba50fc6c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -30,8 +30,7 @@ class DocumentSyncBloc extends Bloc { ); emit( state.copyWith( - shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud, + shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, ), ); _syncStateListener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index d3bbbd27d5..69791f78b7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -14,8 +14,8 @@ import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:cross_file/cross_file.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_impl.dart'; @@ -185,7 +185,7 @@ Future insertLocalFile( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; String? path; String? errorMsg; @@ -230,7 +230,7 @@ Future insertLocalFiles( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 601ba8fa20..6136392884 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -225,8 +225,7 @@ class PageStyleCoverImage extends StatelessWidget { (s) => s, (f) => null, ); - final isAppFlowyCloud = - userProfile?.authType == AuthenticatorPB.AppFlowyCloud; + final isAppFlowyCloud = userProfile?.authType == AuthTypePB.Server; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index c3f04a8837..2356399b4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.authType == AuthenticatorPB.AppFlowyCloud, + (v) => v.authType == AuthTypePB.Server, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index f39fdb01dc..ff8e7b88ec 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -13,7 +13,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/util/default_extensions.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -294,8 +294,8 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = (userProfile?.authType ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); } else { diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 621ba988cf..5a8c0fa651 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -102,7 +102,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { case AuthenticatorType.local: getIt.registerFactory( () => BackendAuthService( - AuthenticatorPB.Local, + AuthTypePB.Local, ), ); break; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 184cc9dd09..362b27a85a 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -112,7 +112,7 @@ class AppFlowyCloudDeepLink { (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( - authenticator: AuthenticatorPB.AppFlowyCloud, + authenticator: AuthTypePB.Server, map: { AuthServiceMapKeys.signInURL: uri.toString(), AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 6d02f188c8..4f4cece9bb 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -18,7 +18,7 @@ class AppFlowyCloudAuthService implements AuthService { AppFlowyCloudAuthService(); final BackendAuthService _backendAuthService = BackendAuthService( - AuthenticatorPB.AppFlowyCloud, + AuthTypePB.Server, ); @override diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index d8cee89b59..8be71dc648 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -20,7 +20,7 @@ class AppFlowyCloudMockAuthService implements AuthService { final String userEmail; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthenticatorPB.AppFlowyCloud); + BackendAuthService(AuthTypePB.Server); @override Future> signUp({ @@ -48,7 +48,7 @@ class AppFlowyCloudMockAuthService implements AuthService { Map params = const {}, }) async { final payload = SignInUrlPayloadPB.create() - ..authenticator = AuthenticatorPB.AppFlowyCloud + ..authenticator = AuthTypePB.Server // don't use nanoid here, the gotrue server will transform the email ..email = userEmail; @@ -58,7 +58,7 @@ class AppFlowyCloudMockAuthService implements AuthService { return getSignInURLResult.fold( (urlPB) async { final payload = OauthSignInPB( - authenticator: AuthenticatorPB.AppFlowyCloud, + authenticator: AuthTypePB.Server, map: { AuthServiceMapKeys.signInURL: urlPB.signInUrl, AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index f47fd5a4a6..cab8cd170c 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,7 +16,7 @@ import 'device_id.dart'; class BackendAuthService implements AuthService { BackendAuthService(this.authType); - final AuthenticatorPB authType; + final AuthTypePB authType; @override Future> @@ -71,7 +72,7 @@ class BackendAuthService implements AuthService { ..email = userEmail ..password = password // When sign up as guest, the auth type is always local. - ..authType = AuthenticatorPB.Local + ..authType = AuthTypePB.Local ..deviceId = await getDeviceId(); final response = await UserEventSignUp(request).send().then( (value) => value, @@ -82,7 +83,7 @@ class BackendAuthService implements AuthService { @override Future> signUpWithOAuth({ required String platform, - AuthenticatorPB authType = AuthenticatorPB.Local, + AuthTypePB authType = AuthTypePB.Local, Map params = const {}, }) async { return FlowyResult.failure( diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart index d421440d08..b85efe38ae 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -4,8 +4,8 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/password/password_http_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/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -46,7 +46,7 @@ class PasswordBloc extends Bloc { bool _isInitialized = false; Future _init() async { - if (userProfile.authType == AuthenticatorPB.Local) { + if (userProfile.authType == AuthTypePB.Local) { Log.debug('PasswordBloc: skip init because user is local authenticator'); return; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index 014c7caaf8..c8744fb304 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,7 +74,7 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.authType != AuthenticatorPB.Local; + final isDisabled = isSelected || user.authType != AuthTypePB.Local; final desc = "${user.name}\t ${user.authType}\t"; final child = SizedBox( height: 30, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 66277cf30b..ce659d5262 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -2,8 +2,6 @@ import 'package:appflowy/user/application/user_listener.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/auth.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; @@ -91,7 +89,7 @@ class SettingsDialogBloc AFRolePB? currentWorkspaceMemberRole, ]) async { if ([ - AuthenticatorPB.Local, + AuthTypePB.Local, ].contains(userProfile.authType)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 44e9fa3f92..5ed56b890e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.authType == AuthenticatorPB.AppFlowyCloud && + userProfile.authType == AuthTypePB.Server && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index 0db1ac9443..ed06f16c8f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -66,7 +66,7 @@ class WorkspaceBloc extends Bloc { Emitter emit, ) async { final result = - await userService.createUserWorkspace(name, AuthTypePB.CloudAuthType); + await userService.createUserWorkspace(name, AuthTypePB.Server); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 24a122fee5..c9f464d43a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.CloudAuthType, + AuthTypePB.Local, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 576ff07cb4..7ccd03b4f4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.authType != AuthenticatorPB.AppFlowyCloud) { + if (userProfile.authType != AuthTypePB.Server) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index f1ecfda8ad..e242da473b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -7,8 +7,8 @@ import 'package:appflowy/workspace/presentation/settings/pages/account/account.d import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.authType != AuthenticatorPB.Local) ...[ + state.userProfile.authType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ @@ -82,30 +82,26 @@ class _SettingsAccountViewState extends State { ), AccountSignInOutSection( userProfile: state.userProfile, - onAction: - state.userProfile.authType == AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: - state.userProfile.authType == AuthenticatorPB.Local, + onAction: state.userProfile.authType == AuthTypePB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authType == AuthTypePB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.authType == AuthenticatorPB.Local) ...[ + state.userProfile.authType == AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: - state.userProfile.authType == AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: - state.userProfile.authType == AuthenticatorPB.Local, + onAction: state.userProfile.authType == AuthTypePB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authType == AuthTypePB.Local, ), ], ), @@ -120,7 +116,7 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthTypePB.Server) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index a602849527..9a17016e5f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.authType != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.authType != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthTypePB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index b17a9beb7e..e262a27cb6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -140,7 +140,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.authType == AuthenticatorPB.AppFlowyCloud) { + if (user.authType == AuthTypePB.Server) { return SettingsAIView( key: ValueKey(workspaceId), userProfile: user, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 1a7144993f..04a93656ca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -63,7 +63,7 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.authType == AuthenticatorPB.AppFlowyCloud) + userProfile.authType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, @@ -109,7 +109,7 @@ class SettingsMenu extends StatelessWidget { ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (userProfile.authType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 3b81d2a041..fe202e7590 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.authType == AuthenticatorPB.AppFlowyCloud) { + userProfile.authType == AuthTypePB.Server) { return const SizedBox.shrink(); } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 1b54087ee1..ff0a3847df 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -8,7 +8,7 @@ use collab_entity::CollabType; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_user::entities::AuthenticatorPB; +use flowy_user::entities::AuthTypePB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; use nanoid::nanoid; @@ -59,7 +59,7 @@ impl EventIntegrationTest { let clean_path = config.storage_path.clone(); let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); - let authenticator = Arc::new(AtomicU8::new(AuthenticatorPB::Local as u8)); + let authenticator = Arc::new(AtomicU8::new(AuthTypePB::Local as u8)); register_notification_sender(notification_sender.clone()); // In case of dropping the runtime that runs the core, we need to forget the dispatcher diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 5c9be65660..c4e34f27f7 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,7 +17,7 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, + AuthTypePB, AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, @@ -65,7 +65,7 @@ impl EventIntegrationTest { email, name: "appflowy".to_string(), password: password.clone(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: uuid::Uuid::new_v4().to_string(), } .into_bytes() @@ -113,7 +113,7 @@ impl EventIntegrationTest { .await; } - pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { + pub fn set_auth_type(&self, auth_type: AuthTypePB) { self.authenticator.store(auth_type as u8, Ordering::Release); } @@ -140,7 +140,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthenticatorPB::AppFlowyCloud, + authenticator: AuthTypePB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +155,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthenticatorPB::AppFlowyCloud, + authenticator: AuthTypePB::AppFlowyCloud, }; let user_profile = EventBuilder::new(self.clone()) diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index f74b202860..1abdde509f 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -1,7 +1,7 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_user::entities::AuthenticatorPB; +use flowy_user::entities::AuthTypePB; use crate::util::unzip; @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.auth_type, AuthenticatorPB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthTypePB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 3c73fd01eb..138f6f0258 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -1,6 +1,6 @@ use event_integration_test::user_event::{login_password, unique_email}; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthenticatorPB, SignInPayloadPB, SignUpPayloadPB}; +use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -14,7 +14,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; @@ -40,7 +40,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; @@ -67,7 +67,7 @@ async fn sign_in_with_invalid_password() { email: unique_email(), password, name: "".to_string(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs index 46f8eca9c6..47c2d53a6b 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs @@ -1,6 +1,6 @@ use crate::user::local_test::helper::*; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use nanoid::nanoid; #[tokio::test] @@ -24,7 +24,7 @@ async fn anon_user_profile_get() { .await .parse::(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.auth_type, AuthenticatorPB::Local); + assert_eq!(user_profile.auth_type, AuthTypePB::Local); } #[tokio::test] diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index eec50b1480..14d39690f9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -35,7 +35,7 @@ impl UserCloudService for LocalServerUserServiceImpl { let params = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); let workspace_id = Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new_local(&workspace_id, uid); + let user_workspace = UserWorkspace::new_local(workspace_id, ""); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() } else { @@ -59,7 +59,9 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn sign_in(&self, params: BoxAny) -> Result { let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let user_workspace = make_user_workspace("My Workspace"); + + let workspace_id = Uuid::new_v4(); + let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), "My Workspace"); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -143,7 +145,11 @@ impl UserCloudService for LocalServerUserServiceImpl { } async fn create_workspace(&self, workspace_name: &str) -> Result { - Ok(make_user_workspace(workspace_name)) + let workspace_id = Uuid::new_v4(); + Ok(UserWorkspace::new_local( + workspace_id.to_string(), + workspace_name, + )) } async fn patch_workspace( @@ -192,15 +198,3 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } } - -fn make_user_workspace(name: &str) -> UserWorkspace { - UserWorkspace { - id: Uuid::new_v4().to_string(), - name: name.to_string(), - created_at: Default::default(), - workspace_database_id: Uuid::new_v4().to_string(), - icon: "".to_string(), - member_count: 1, - role: None, - } -} diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 0d91f84b0e..efceb8b5f6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -122,10 +122,10 @@ impl UserWorkspace { Ok(id) } - pub fn new_local(workspace_id: &str, _uid: i64) -> Self { + pub fn new_local(workspace_id: String, name: &str) -> Self { Self { - id: workspace_id.to_string(), - name: "".to_string(), + id: workspace_id, + name: name.to_string(), created_at: Utc::now(), workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index a1165c9621..0570020716 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -93,20 +93,20 @@ pub fn upsert_user_workspace( user_workspace: UserWorkspace, conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { - let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; + let row = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) + .values(row.clone()) .on_conflict(user_workspace_table::id) .do_update() .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - user_workspace_table::auth_type.eq(new_record.auth_type), + user_workspace_table::name.eq(row.name), + user_workspace_table::uid.eq(row.uid), + user_workspace_table::created_at.eq(row.created_at), + user_workspace_table::database_storage_id.eq(row.database_storage_id), + user_workspace_table::icon.eq(row.icon), + user_workspace_table::member_count.eq(row.member_count), + user_workspace_table::role.eq(row.role), + user_workspace_table::auth_type.eq(row.auth_type), )) .execute(conn)?; diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index ddc0dc29f9..a61ba5cc96 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; use std::convert::TryInto; +use crate::entities::parser::*; +use crate::entities::AuthTypePB; +use crate::errors::ErrorCode; use client_api::entity::GotrueTokenResponse; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; -use crate::entities::parser::*; -use crate::errors::ErrorCode; - #[derive(ProtoBuf, Default)] pub struct SignInPayloadPB { #[pb(index = 1)] @@ -20,7 +20,7 @@ pub struct SignInPayloadPB { pub name: String, #[pb(index = 4)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, #[pb(index = 5)] pub device_id: String, @@ -53,7 +53,7 @@ pub struct SignUpPayloadPB { pub password: String, #[pb(index = 4)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, #[pb(index = 5)] pub device_id: String, @@ -144,7 +144,7 @@ pub struct OauthSignInPB { pub map: HashMap, #[pb(index = 2)] - pub authenticator: AuthenticatorPB, + pub authenticator: AuthTypePB, } #[derive(ProtoBuf, Default)] @@ -153,7 +153,7 @@ pub struct SignInUrlPayloadPB { pub email: String, #[pb(index = 2)] - pub authenticator: AuthenticatorPB, + pub authenticator: AuthTypePB, } #[derive(ProtoBuf, Default)] @@ -228,41 +228,10 @@ pub struct OauthProviderDataPB { pub oauth_url: String, } -#[repr(u8)] -#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] -pub enum AuthenticatorPB { - Local = 0, - AppFlowyCloud = 2, -} - -impl From for AuthenticatorPB { - fn from(auth_type: AuthType) -> Self { - match auth_type { - AuthType::Local => AuthenticatorPB::Local, - AuthType::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, - } - } -} - -impl From for AuthType { - fn from(pb: AuthenticatorPB) -> Self { - match pb { - AuthenticatorPB::Local => AuthType::Local, - AuthenticatorPB::AppFlowyCloud => AuthType::AppFlowyCloud, - } - } -} - -impl Default for AuthenticatorPB { - fn default() -> Self { - Self::Local - } -} - #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, } #[derive(ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 17b951bae0..5afc93850a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,6 +1,6 @@ use super::AFRolePB; use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::{AuthTypePB, AuthenticatorPB}; +use crate::entities::AuthTypePB; use crate::errors::ErrorCode; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; @@ -39,7 +39,7 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index e178b724db..ed8eb8aa76 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -242,18 +242,23 @@ pub struct CreateWorkspacePB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Default, Debug, Clone)] +#[derive(ProtoBuf_Enum, Default, Debug, Clone, Eq, PartialEq)] +#[repr(u8)] pub enum AuthTypePB { - LocalAuthType = 0, #[default] - CloudAuthType = 1, + Local = 0, + Server = 1, } + impl From for AuthTypePB { fn from(value: i32) -> Self { match value { - 0 => AuthTypePB::LocalAuthType, - 1 => AuthTypePB::CloudAuthType, - _ => AuthTypePB::CloudAuthType, + 0 => AuthTypePB::Local, + 1 => AuthTypePB::Server, + // For historical reasons, 2 also maps to Server. + // Check the AuthenticatorType in flowy-server-pub + 2 => AuthTypePB::Server, + _ => AuthTypePB::Server, } } } @@ -261,8 +266,8 @@ impl From for AuthTypePB { impl From for AuthTypePB { fn from(value: AuthType) -> Self { match value { - AuthType::Local => AuthTypePB::LocalAuthType, - AuthType::AppFlowyCloud => AuthTypePB::CloudAuthType, + AuthType::Local => AuthTypePB::Local, + AuthType::AppFlowyCloud => AuthTypePB::Server, } } } @@ -270,8 +275,8 @@ impl From for AuthTypePB { impl From for AuthType { fn from(value: AuthTypePB) -> Self { match value { - AuthTypePB::LocalAuthType => AuthType::Local, - AuthTypePB::CloudAuthType => AuthType::AppFlowyCloud, + AuthTypePB::Local => AuthType::Local, + AuthTypePB::Server => AuthType::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 17565e83df..6c786d9435 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -218,15 +218,23 @@ impl UserManager { workspace_name: &str, auth_type: AuthType, ) -> FlowyResult { - let new_workspace = self - .cloud_service - .get_user_service()? - .create_workspace(workspace_name) - .await?; + let new_workspace = match auth_type { + AuthType::Local => { + let workspace_id = Uuid::new_v4(); + UserWorkspace::new_local(workspace_id.to_string(), workspace_name) + }, + AuthType::AppFlowyCloud => { + self + .cloud_service + .get_user_service()? + .create_workspace(workspace_name) + .await? + }, + }; info!( - "new workspace: {}, name:{}", - new_workspace.id, new_workspace.name + "create workspace: {}, name:{}, auth_type: {}", + new_workspace.id, new_workspace.name, auth_type ); // save the workspace to sqlite db From 2c5f41b58062771135fa4dd77c6b3e8777c7c2db Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 10:53:21 +0800 Subject: [PATCH 374/384] fix: set auth type --- .../settings/settings_dialog_bloc.dart | 1 + .../workspace/_sidebar_workspace_menu.dart | 2 +- .../user/af_cloud_test/anon_user_test.rs | 2 +- .../rust-lib/flowy-core/src/server_layer.rs | 10 ++--- .../flowy-core/src/user_state_callback.rs | 6 --- .../src/local_server/impls/folder.rs | 2 +- .../src/local_server/impls/user.rs | 3 +- .../flowy-user-pub/src/sql/workspace_sql.rs | 1 - .../flowy-user/src/entities/workspace.rs | 3 -- .../rust-lib/flowy-user/src/event_handler.rs | 14 +----- .../flowy-user/src/user_manager/manager.rs | 45 ++++++------------- .../user_manager/manager_user_workspace.rs | 2 + 12 files changed, 29 insertions(+), 62 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index ce659d5262..726e95bb9e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_listener.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/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index c9f464d43a..097da2c2ae 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 1abdde509f..7f743b931c 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.auth_type, AuthTypePB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthTypePB::Server); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 4b1834c41c..b666ab4749 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -82,12 +82,12 @@ impl ServerProvider { pub fn set_auth_type(&self, new_auth_type: AuthType) { let old_type = self.get_auth_type(); - info!( - "ServerProvider: set auth type from {:?} to {:?}", - old_type, new_auth_type - ); - if old_type != new_auth_type { + info!( + "ServerProvider: auth type from {:?} to {:?}", + old_type, new_auth_type + ); + self.auth_type.store(Arc::new(new_auth_type)); if let Some((auth_type, _)) = self.providers.remove(&old_type) { info!("ServerProvider: remove old auth type: {:?}", auth_type); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index e85b773ec4..f3eaa00543 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -55,7 +55,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { auth_type: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; - self.server_provider.set_auth_type(*auth_type); if let Some(cloud_config) = cloud_config { self @@ -102,8 +101,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); - self.server_provider.set_auth_type(*auth_type); - self .folder_manager .initialize_after_sign_in(user_id) @@ -130,8 +127,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { device_id: &str, auth_type: &AuthType, ) -> FlowyResult<()> { - self.server_provider.set_auth_type(*auth_type); - event!( tracing::Level::TRACE, "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", @@ -212,7 +207,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace: &UserWorkspace, auth_type: &AuthType, ) -> FlowyResult<()> { - self.server_provider.set_auth_type(*auth_type); self .folder_manager .initialize_after_open_workspace(user_id) diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 31a65e7fa9..72bab514ec 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -84,7 +84,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { workspace_id: &Uuid, view_ids: Vec, ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) + Ok(()) } async fn get_publish_info(&self, view_id: &Uuid) -> Result { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 14d39690f9..e73943bbbe 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -48,7 +48,8 @@ impl UserCloudService for LocalServerUserServiceImpl { latest_workspace: user_workspace.clone(), user_workspaces: vec![user_workspace], is_new_user: true, - email: Some(params.email), + // Anon user doesn't have email + email: None, token: None, encryption_type: EncryptionType::NoEncryption, updated_at: timestamp(), diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index 0570020716..cb6d299039 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -172,7 +172,6 @@ pub fn delete_all_then_insert_user_workspaces( ) -> FlowyResult<()> { conn.immediate_transaction(|conn| { delete_user_all_workspace(uid, auth_type, conn)?; - info!( "Insert {} workspaces for user {} and auth type {:?}", user_workspaces.len(), diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index ed8eb8aa76..99544eede4 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -255,9 +255,6 @@ impl From for AuthTypePB { match value { 0 => AuthTypePB::Local, 1 => AuthTypePB::Server, - // For historical reasons, 2 also maps to Server. - // Check the AuthenticatorType in flowy-server-pub - 2 => AuthTypePB::Server, _ => AuthTypePB::Server, } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bb38a9e0a0..905eb595f1 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -44,18 +44,12 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_service.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), - Err(err) => { - manager - .cloud_service - .set_server_auth_type(&old_authenticator); - return Err(err); - }, + Err(err) => Err(err), } } @@ -77,13 +71,9 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_service.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), - Err(err) => { - manager.cloud_service.set_server_auth_type(&prev_auth_type); - Err(err) - }, + Err(err) => Err(err), } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 6cfa8bf16f..9520489771 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -132,18 +132,19 @@ impl UserManager { if let Ok(session) = self.get_session() { let user = self.get_user_profile_from_disk(session.user_id).await?; + self.cloud_service.set_server_auth_type(&user.auth_type); // Get the current authenticator from the environment variable - let current_authenticator = current_authenticator(); + let env_auth_type = current_authenticator(); // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.auth_type != AuthType::Local && user.auth_type != current_authenticator { + if user.auth_type != AuthType::Local && user.auth_type != env_auth_type { event!( tracing::Level::INFO, - "Authenticator changed from {:?} to {:?}", + "Auth type changed from {:?} to {:?}", user.auth_type, - current_authenticator + env_auth_type ); self.sign_out().await?; return Ok(()); @@ -151,7 +152,7 @@ impl UserManager { event!( tracing::Level::INFO, - "init user session: {}:{}, authenticator: {:?}", + "init user session: {}:{}, auth type: {:?}", user.uid, user.email, user.auth_type, @@ -389,9 +390,10 @@ impl UserManager { auth_type: AuthType, params: BoxAny, ) -> Result { + self.cloud_service.set_server_auth_type(&auth_type); + // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_service.set_server_auth_type(&auth_type); let auth_service = self.cloud_service.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); @@ -401,28 +403,6 @@ impl UserManager { Ok(new_user_profile) } - #[tracing::instrument(level = "info", skip(self))] - pub async fn resume_sign_up(&self) -> Result<(), FlowyError> { - let UserAuthProcess { - user_profile, - migration_user, - response, - authenticator, - } = self - .auth_process - .lock() - .await - .clone() - .ok_or(FlowyError::new( - ErrorCode::Internal, - "No resumable sign up data", - ))?; - self - .continue_sign_up(&user_profile, migration_user, response, &authenticator) - .await?; - Ok(()) - } - #[tracing::instrument(level = "info", skip_all, err)] async fn continue_sign_up( &self, @@ -436,9 +416,7 @@ impl UserManager { self .save_auth_data(&response, *auth_type, &new_session) .await?; - let _ = self - .initial_user_awareness(&new_session, &new_user_profile.auth_type) - .await; + let _ = self.initial_user_awareness(&new_session, auth_type).await; self .user_status_callback .read() @@ -670,6 +648,7 @@ impl UserManager { } } + #[instrument(level = "info", skip_all)] pub(crate) async fn generate_sign_in_url_with_email( &self, authenticator: &AuthType, @@ -682,6 +661,7 @@ impl UserManager { Ok(url) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_password( &self, email: &str, @@ -695,6 +675,7 @@ impl UserManager { Ok(response) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_magic_link( &self, email: &str, @@ -710,6 +691,7 @@ impl UserManager { Ok(()) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_passcode( &self, email: &str, @@ -723,6 +705,7 @@ impl UserManager { Ok(response) } + #[instrument(level = "info", skip_all)] pub(crate) async fn generate_oauth_url( &self, oauth_provider: &str, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 6c786d9435..04e7e6b0d9 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -154,6 +154,8 @@ impl UserManager { #[instrument(skip(self), err)] pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + self.cloud_service.set_server_auth_type(&auth_type); + let uid = self.user_id()?; let conn = self.db_connection(self.user_id()?)?; let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { From ccd1f5f8e9742a86c023664edb2d17e848a1c6ef Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 12:47:55 +0800 Subject: [PATCH 375/384] chore: revert pod lock --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 92e52a1a79..4b7ed5d639 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 - appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 + appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 - flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 - keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 + saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca From bb5d36402aa1396d4b76429a7ddd4ec265cc5257 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 13:35:06 +0800 Subject: [PATCH 376/384] chore: fix test --- .../event-integration-test/src/user_event.rs | 12 ++++---- .../flowy-core/src/user_state_callback.rs | 16 +++++----- .../rust-lib/flowy-user/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-user/src/event_map.rs | 29 ++++++++++--------- .../flowy-user/src/user_manager/manager.rs | 12 +++----- .../user_manager/manager_user_workspace.rs | 6 ++-- .../flowy-user/src/user_manager/mod.rs | 1 - .../src/user_manager/user_login_state.rs | 11 ------- 8 files changed, 38 insertions(+), 51 deletions(-) delete mode 100644 frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index c4e34f27f7..ab10bb7083 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,10 +17,10 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, - ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, - RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, - UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, + AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, + OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, + SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, + UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -140,7 +140,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthTypePB::AppFlowyCloud, + authenticator: AuthTypePB::Server, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +155,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthTypePB::AppFlowyCloud, + authenticator: AuthTypePB::Server, }; let user_profile = EventBuilder::new(self.clone()) diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index f3eaa00543..e43002d709 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -46,7 +46,7 @@ impl UserStatusCallbackImpl { #[async_trait] impl UserStatusCallback for UserStatusCallbackImpl { - async fn did_init( + async fn on_launch_if_authenticated( &self, user_id: i64, cloud_config: &Option, @@ -88,7 +88,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_sign_in( + async fn on_sign_in( &self, user_id: i64, user_workspace: &UserWorkspace, @@ -119,7 +119,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_sign_up( + async fn on_sign_up( &self, is_new_user: bool, user_profile: &UserProfile, @@ -196,12 +196,12 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { + async fn on_token_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { self.folder_manager.clear(user_id).await; Ok(()) } - async fn open_workspace( + async fn on_workspace_opened( &self, user_id: i64, user_workspace: &UserWorkspace, @@ -230,13 +230,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - fn did_update_network(&self, reachable: bool) { + fn on_network_status_changed(&self, reachable: bool) { info!("Notify did update network: reachable: {}", reachable); self.collab_builder.update_network(reachable); self.storage_manager.update_network_reachable(reachable); } - fn did_update_plans(&self, plans: Vec) { + fn on_subscription_plans_updated(&self, plans: Vec) { let mut storage_plan_changed = false; for plan in &plans { match plan { @@ -249,7 +249,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } - fn did_update_storage_limitation(&self, can_write: bool) { + fn on_storage_permission_updated(&self, can_write: bool) { if can_write { self.storage_manager.enable_storage_write_access(); } else { diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 905eb595f1..b1ce8c5ec6 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -475,7 +475,7 @@ pub async fn update_network_state_handler( .user_status_callback .read() .await - .did_update_network(reachable); + .on_network_status_changed(reachable); Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 1ae414afa7..0b489e6f28 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -279,10 +279,9 @@ pub enum UserEvent { pub trait UserStatusCallback: Send + Sync + 'static { /// When the [AuthType] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn authenticator_did_changed(&self, _authenticator: AuthType) {} - /// This will be called after the application launches if the user is already signed in. - /// If the user is not signed in, this method will not be called - async fn did_init( + fn on_auth_type_changed(&self, _authenticator: AuthType) {} + /// Fires on app launch, but only if the user is already signed in. + async fn on_launch_if_authenticated( &self, _user_id: i64, _cloud_config: &Option, @@ -292,8 +291,8 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - /// Will be called after the user signed in. - async fn did_sign_in( + /// Fires right after the user successfully signs in. + async fn on_sign_in( &self, _user_id: i64, _user_workspace: &UserWorkspace, @@ -302,8 +301,9 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - /// Will be called after the user signed up. - async fn did_sign_up( + + /// Fires right after the user successfully signs up. + async fn on_sign_up( &self, _is_new_user: bool, _user_profile: &UserProfile, @@ -314,10 +314,13 @@ pub trait UserStatusCallback: Send + Sync + 'static { Ok(()) } - async fn did_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { + /// Fires when an authentication token has expired. + async fn on_token_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { Ok(()) } - async fn open_workspace( + + /// Fires when a workspace is opened by the user. + async fn on_workspace_opened( &self, _user_id: i64, _user_workspace: &UserWorkspace, @@ -325,9 +328,9 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - fn did_update_network(&self, _reachable: bool) {} - fn did_update_plans(&self, _plans: Vec) {} - fn did_update_storage_limitation(&self, _can_write: bool) {} + fn on_network_status_changed(&self, _reachable: bool) {} + fn on_subscription_plans_updated(&self, _plans: Vec) {} + fn on_storage_permission_updated(&self, _can_write: bool) {} } /// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 9520489771..f4c09d0af8 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,7 +1,7 @@ use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::{internal_error, ErrorCode, FlowyResult}; +use flowy_error::{internal_error, FlowyResult}; use arc_swap::ArcSwapOption; use collab::lock::RwLock; @@ -21,7 +21,6 @@ use serde_json::Value; use std::string::ToString; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; -use tokio::sync::Mutex; use tokio_stream::StreamExt; use tracing::{debug, error, event, info, instrument, warn}; use uuid::Uuid; @@ -40,7 +39,6 @@ use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; -use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; use flowy_user_pub::sql::*; @@ -53,7 +51,6 @@ pub struct UserManager { pub(crate) collab_builder: Weak, pub(crate) collab_interact: RwLock>, pub(crate) user_workspace_service: Arc, - auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, pub(crate) is_loading_awareness: Arc>, @@ -78,7 +75,6 @@ impl UserManager { user_status_callback, collab_builder, collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), - auth_process: Default::default(), authenticate_user, refresh_user_profile_since, user_workspace_service, @@ -270,7 +266,7 @@ impl UserManager { let _ = self.initial_user_awareness(&session, &user.auth_type).await; user_status_callback - .did_init( + .on_launch_if_authenticated( user.uid, &cloud_config, &session.user_workspace, @@ -363,7 +359,7 @@ impl UserManager { .user_status_callback .read() .await - .did_sign_in( + .on_sign_in( user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, @@ -421,7 +417,7 @@ impl UserManager { .user_status_callback .read() .await - .did_sign_up( + .on_sign_up( response.is_new_user, new_user_profile, &new_session.user_workspace, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 04e7e6b0d9..20acb9866f 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -195,7 +195,7 @@ impl UserManager { .user_status_callback .read() .await - .open_workspace(uid, &user_workspace, &user_profile.auth_type) + .on_workspace_opened(uid, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); @@ -532,7 +532,7 @@ impl UserManager { .user_status_callback .read() .await - .did_update_storage_limitation(can_write); + .on_storage_permission_updated(can_write); Ok(workspace_usage) } @@ -693,7 +693,7 @@ impl UserManager { .user_status_callback .read() .await - .did_update_plans(plans); + .on_subscription_plans_updated(plans); Ok(()) } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs index 3ce66227c5..23c050c1f2 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs @@ -3,6 +3,5 @@ pub(crate) mod manager_history_user; pub(crate) mod manager_user_awareness; pub(crate) mod manager_user_encryption; pub(crate) mod manager_user_workspace; -mod user_login_state; pub use manager::*; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs deleted file mode 100644 index ed7fdacf8d..0000000000 --- a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::migrations::AnonUser; -use flowy_user_pub::entities::{AuthResponse, AuthType, UserProfile}; - -/// recording the intermediate state of the sign-in/sign-up process -#[derive(Clone)] -pub struct UserAuthProcess { - pub user_profile: UserProfile, - pub response: AuthResponse, - pub authenticator: AuthType, - pub migration_user: Option, -} From 747a63d452507c201d2d0007a8fd2868f8cce9fc Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 14:33:05 +0800 Subject: [PATCH 377/384] chore: create user workspace for anon user --- .../home/mobile_home_page_header.dart | 2 +- .../application/user/user_workspace_bloc.dart | 12 ++-- .../workspace/_sidebar_workspace_menu.dart | 2 +- .../user/af_cloud_test/workspace_test.rs | 23 +++++--- .../src/local_server/impls/user.rs | 4 +- .../up.sql | 4 +- frontend/rust-lib/flowy-sqlite/src/schema.rs | 26 ++++----- .../flowy-user-pub/src/sql/workspace_sql.rs | 12 ++-- .../flowy-user/src/entities/user_profile.rs | 6 +- .../src/migrations/anon_user_workspace.rs | 58 +++++++++++++++++++ .../src/migrations/doc_key_with_workspace.rs | 2 + .../src/migrations/document_empty_content.rs | 2 + .../flowy-user/src/migrations/migration.rs | 5 +- .../rust-lib/flowy-user/src/migrations/mod.rs | 1 + .../migrations/workspace_and_favorite_v1.rs | 2 + .../src/migrations/workspace_trash_v1.rs | 2 + .../flowy-user/src/user_manager/manager.rs | 2 + .../user_manager/manager_user_workspace.rs | 8 +-- 18 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 4a1df63740..113f12e543 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -194,7 +194,7 @@ class _MobileWorkspace extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.authType, + workspace.workspaceAuthType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 5ed56b890e..0e0b912a08 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -54,7 +54,7 @@ class UserWorkspaceBloc extends Bloc { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); await _userService.openWorkspace( currentWorkspace.workspaceId, - currentWorkspace.authType, + currentWorkspace.workspaceAuthType, ); } @@ -92,7 +92,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( currentWorkspace.workspaceId, - currentWorkspace.authType, + currentWorkspace.workspaceAuthType, ), ); } @@ -132,7 +132,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( s.workspaceId, - s.authType, + s.workspaceAuthType, ), ); }) @@ -190,7 +190,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } @@ -203,7 +203,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } @@ -369,7 +369,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 097da2c2ae..4ff5ccbf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -309,7 +309,7 @@ class _WorkspaceInfo extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.authType, + workspace.workspaceAuthType, ), ); diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index ea04d922fc..3bb71ea0dc 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -90,7 +90,10 @@ async fn af_cloud_create_workspace_test() { { // after opening new workspace test - .open_workspace(&created_workspace.workspace_id, created_workspace.auth_type) + .open_workspace( + &created_workspace.workspace_id, + created_workspace.workspace_auth_type, + ) .await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); @@ -124,7 +127,10 @@ async fn af_cloud_open_workspace_test() { .create_workspace("second workspace", AuthType::AppFlowyCloud) .await; test - .open_workspace(&user_workspace.workspace_id, user_workspace.auth_type) + .open_workspace( + &user_workspace.workspace_id, + user_workspace.workspace_auth_type, + ) .await; let second_workspace = test.get_current_workspace().await; let second_workspace = test.get_user_workspace(&second_workspace.id).await; @@ -144,7 +150,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.auth_type.clone(), + first_workspace.workspace_auth_type.clone(), ) .await; sleep(Duration::from_millis(300)).await; @@ -155,7 +161,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.auth_type.clone(), + second_workspace.workspace_auth_type.clone(), ) .await; sleep(Duration::from_millis(200)).await; @@ -168,7 +174,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.auth_type.clone(), + first_workspace.workspace_auth_type.clone(), ) .await; let views_1 = test.get_all_workspace_views().await; @@ -180,7 +186,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.auth_type.clone(), + second_workspace.workspace_auth_type.clone(), ) .await; let views_2 = test.get_all_workspace_views().await; @@ -239,7 +245,10 @@ async fn af_cloud_different_open_same_workspace_test() { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; client - .open_workspace(iter_workspace_id, all_workspaces[index].auth_type.clone()) + .open_workspace( + iter_workspace_id, + all_workspaces[index].workspace_auth_type.clone(), + ) .await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index e73943bbbe..512ad90a22 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -133,9 +133,9 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn open_workspace(&self, workspace_id: &Uuid) -> Result { let uid = self.user.user_id()?; - let conn = self.user.get_sqlite_db(uid)?; + let mut conn = self.user.get_sqlite_db(uid)?; - let workspace = select_user_workspace(&workspace_id.to_string(), conn)?; + let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; Ok(UserWorkspace::from(workspace)) } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql index b77372ad63..7d986e3e57 100644 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql @@ -1,10 +1,10 @@ -- Your SQL goes here ALTER TABLE user_workspace_table - ADD COLUMN auth_type INTEGER NOT NULL DEFAULT 1; + ADD COLUMN workspace_type INTEGER NOT NULL DEFAULT 1; -- 2. Back‑fill from user_table.auth_type UPDATE user_workspace_table -SET auth_type = (SELECT ut.auth_type +SET workspace_type = (SELECT ut.auth_type FROM user_table ut WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) WHERE EXISTS (SELECT 1 diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 28a83ed449..eb6b248ea6 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -107,7 +107,7 @@ diesel::table! { icon -> Text, member_count -> BigInt, role -> Nullable, - auth_type -> Integer, + workspace_type -> Integer, } } @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index cb6d299039..709e218514 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -17,7 +17,7 @@ pub struct UserWorkspaceTable { pub icon: String, pub member_count: i64, pub role: Option, - pub auth_type: i32, + pub workspace_type: i32, } #[derive(AsChangeset, Identifiable, Default, Debug)] @@ -50,18 +50,18 @@ impl UserWorkspaceTable { icon: workspace.icon.clone(), member_count: workspace.member_count, role: workspace.role.clone().map(|v| v as i32), - auth_type: auth_type as i32, + workspace_type: auth_type as i32, }) } } pub fn select_user_workspace( workspace_id: &str, - mut conn: DBConnection, + conn: &mut SqliteConnection, ) -> FlowyResult { let row = user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn)?; + .first::(conn)?; Ok(row) } @@ -106,7 +106,7 @@ pub fn upsert_user_workspace( user_workspace_table::icon.eq(row.icon), user_workspace_table::member_count.eq(row.member_count), user_workspace_table::role.eq(row.role), - user_workspace_table::auth_type.eq(row.auth_type), + user_workspace_table::workspace_type.eq(row.workspace_type), )) .execute(conn)?; @@ -153,7 +153,7 @@ pub fn delete_user_all_workspace( let n = diesel::delete( user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + .filter(user_workspace_table::workspace_type.eq(auth_type as i32)), ) .execute(conn)?; info!( diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 5afc93850a..6a95a89041 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -186,7 +186,7 @@ pub struct UserWorkspacePB { pub role: Option, #[pb(index = 7)] - pub auth_type: AuthTypePB, + pub workspace_auth_type: AuthTypePB, } impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { @@ -198,7 +198,7 @@ impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { icon: value.1.icon, member_count: value.1.member_count, role: value.1.role.map(AFRolePB::from), - auth_type: AuthTypePB::from(value.0), + workspace_auth_type: AuthTypePB::from(value.0), } } } @@ -212,7 +212,7 @@ impl From for UserWorkspacePB { icon: value.icon, member_count: value.member_count, role: value.role.map(AFRolePB::from), - auth_type: AuthTypePB::from(value.auth_type), + workspace_auth_type: AuthTypePB::from(value.workspace_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs new file mode 100644 index 0000000000..7c806d3aaf --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs @@ -0,0 +1,58 @@ +use diesel::SqliteConnection; +use semver::Version; +use std::sync::Arc; +use tracing::{info, instrument}; + +use collab_integrate::CollabKVDB; +use flowy_error::FlowyResult; +use flowy_user_pub::entities::AuthType; + +use crate::migrations::migration::UserDataMigration; +use flowy_user_pub::session::Session; +use flowy_user_pub::sql::{select_user_workspace, upsert_user_workspace}; + +pub struct AnonUserWorkspaceTableMigration; + +impl UserDataMigration for AnonUserWorkspaceTableMigration { + fn name(&self) -> &str { + "anon_user_workspace_table_migration" + } + + fn run_when( + &self, + first_installed_version: &Option, + _current_version: &Version, + ) -> bool { + match first_installed_version { + None => true, + Some(version) => version <= &Version::new(0, 8, 10), + } + } + + #[instrument(name = "AnonUserWorkspaceTableMigration", skip_all, err)] + fn run( + &self, + session: &Session, + _collab_db: &Arc, + auth_type: &AuthType, + db: &mut SqliteConnection, + ) -> FlowyResult<()> { + // For historical reason, anon user doesn't have a workspace in user_workspace_table. + // So we need to create a new entry for the anon user in the user_workspace_table. + if matches!(auth_type, AuthType::Local) { + let user_workspace = &session.user_workspace; + let result = select_user_workspace(&user_workspace.id, db); + if let Err(e) = result { + if e.is_record_not_found() { + info!( + "Anon user workspace not found in the database, creating a new entry for user_id: {}", + session.user_id + ); + upsert_user_workspace(session.user_id, *auth_type, user_workspace.clone(), db)?; + } + } + } + + Ok(()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 5a8bb4d516..84acc0b56a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_plugins::local_storage::kv::doc::migrate_old_keys; use collab_plugins::local_storage::kv::KVTransactionDB; +use diesel::SqliteConnection; use semver::Version; use tracing::{instrument, trace}; @@ -40,6 +41,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { trace!( "migrate key with workspace id:{}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index ab828e18d8..2e4581f7ec 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -6,6 +6,7 @@ use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::{Folder, View}; use collab_plugins::local_storage::kv::KVTransactionDB; +use diesel::SqliteConnection; use semver::Version; use tracing::{event, instrument}; @@ -42,6 +43,7 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { session: &Session, collab_db: &Arc, authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 1330d1995b..0f5c2c2624 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - authenticator: &AuthType, + auth_type: &AuthType, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -75,7 +75,7 @@ impl UserLocalDataMigration { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - migration.run(&self.session, &self.collab_db, authenticator)?; + migration.run(&self.session, &self.collab_db, auth_type, &mut conn)?; applied_migrations.push(migration.name().to_string()); save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); @@ -99,6 +99,7 @@ pub trait UserDataMigration { user: &Session, collab_db: &Arc, authenticator: &AuthType, + db: &mut SqliteConnection, ) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index c8d04edf66..3d87dc595f 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,6 +1,7 @@ use flowy_user_pub::session::Session; use std::sync::Arc; +pub mod anon_user_workspace; pub mod doc_key_with_workspace; pub mod document_empty_content; pub mod migration; diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index 51894f9a04..5f14051e26 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use diesel::SqliteConnection; use semver::Version; use tracing::instrument; @@ -40,6 +41,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index 70123edb2c..b5eeead8c6 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use diesel::SqliteConnection; use semver::Version; use tracing::instrument; @@ -38,6 +39,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index f4c09d0af8..4299f0823b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -38,6 +38,7 @@ use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; +use crate::migrations::anon_user_workspace::AnonUserWorkspaceTableMigration; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; @@ -849,6 +850,7 @@ fn collab_migration_list() -> Vec> { Box::new(FavoriteV1AndWorkspaceArrayMigration), Box::new(WorkspaceTrashMapToSectionMigration), Box::new(CollabDocKeyWithWorkspaceIdMigration), + Box::new(AnonUserWorkspaceTableMigration), ] } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 20acb9866f..b68643c83b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -157,8 +157,8 @@ impl UserManager { self.cloud_service.set_server_auth_type(&auth_type); let uid = self.user_id()?; - let conn = self.db_connection(self.user_id()?)?; - let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { + let mut conn = self.db_connection(self.user_id()?)?; + let user_workspace = match select_user_workspace(&workspace_id.to_string(), &mut conn) { Err(err) => { if err.is_record_not_found() { sync_workspace( @@ -401,8 +401,8 @@ impl UserManager { uid: i64, workspace_id: &Uuid, ) -> FlowyResult { - let conn = self.db_connection(uid)?; - select_user_workspace(workspace_id.to_string().as_str(), conn) + let mut conn = self.db_connection(uid)?; + select_user_workspace(workspace_id.to_string().as_str(), &mut conn) } pub async fn get_all_user_workspaces( From fd581b44535894540c5e9e2a2f6240cb47cf0642 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 14:37:21 +0800 Subject: [PATCH 378/384] chore: clippy --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index eb6b248ea6..f91d187b75 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); From fa798f3ecd95a5f98d1e0891bb68abac00f7b6a2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 15:54:37 +0800 Subject: [PATCH 379/384] chore: workspace usage --- .../workspace/_sidebar_workspace_menu.dart | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 5 + .../flowy-server/src/af_cloud/define.rs | 4 + .../src/local_server/impls/chat.rs | 34 ++-- .../src/local_server/impls/folder.rs | 7 +- .../src/local_server/impls/user.rs | 153 ++++++++++++++++-- .../flowy-server/src/local_server/server.rs | 14 +- .../flowy-server/tests/af_cloud_test/util.rs | 10 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 21 +-- .../flowy-user-pub/src/sql/member_sql.rs | 18 ++- .../flowy-user-pub/src/sql/user_sql.rs | 18 ++- .../src/sql/workspace_setting_sql.rs | 8 +- .../data_import/appflowy_data_import.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 4 +- .../flowy-user/src/user_manager/manager.rs | 21 ++- .../user_manager/manager_user_workspace.rs | 4 +- 16 files changed, 241 insertions(+), 84 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 4ff5ccbf67..04eb701a66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Server, + AuthTypePB.Local, ), ); }, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 7e6d477407..893a010a18 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused_doc_comments)] use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use collab_plugins::CollabKVDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; @@ -342,6 +343,10 @@ impl LoggedUser for ServerUserImpl { self.upgrade_user()?.get_sqlite_connection(uid) } + fn get_collab_db(&self, uid: i64) -> Result, FlowyError> { + self.upgrade_user()?.get_collab_db(uid) + } + fn application_root_dir(&self) -> Result { Ok(PathBuf::from( self.upgrade_user()?.get_application_root_dir(), diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index a93066054d..65808e5b6b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,3 +1,4 @@ +use collab_plugins::CollabKVDB; use flowy_ai::ai_manager::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; @@ -21,6 +22,9 @@ pub trait LoggedUser: Send + Sync { async fn is_local_mode(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; + + fn get_collab_db(&self, uid: i64) -> Result, FlowyError>; + fn application_root_dir(&self) -> Result; } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index f56a8d6e8b..3af87eb18e 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -28,14 +28,14 @@ use tracing::trace; use uuid::Uuid; pub struct LocalChatServiceImpl { - pub user: Arc, + pub logged_user: Arc, pub local_ai: Arc, } impl LocalChatServiceImpl { fn get_message_content(&self, message_id: i64) -> FlowyResult { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let content = select_message_content(db, message_id)?.ok_or_else(|| { FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) })?; @@ -43,8 +43,8 @@ impl LocalChatServiceImpl { } async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let conn = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let conn = self.logged_user.get_sqlite_db(uid)?; let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); upsert_chat_messages(conn, &[row])?; Ok(()) @@ -62,8 +62,8 @@ impl ChatCloudService for LocalChatServiceImpl { _name: &str, metadata: Value, ) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); upsert_chat(db, &row)?; Ok(()) @@ -139,8 +139,8 @@ impl ChatCloudService for LocalChatServiceImpl { chat_id: &Uuid, question_id: i64, ) -> Result { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { None => Err(FlowyError::record_not_found()), @@ -156,8 +156,8 @@ impl ChatCloudService for LocalChatServiceImpl { limit: u64, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let result = select_chat_messages(db, &chat_id, limit, offset)?; let messages = result @@ -180,8 +180,8 @@ impl ChatCloudService for LocalChatServiceImpl { answer_message_id: i64, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? .map(chat_message_from_row) .ok_or_else(FlowyError::record_not_found)?; @@ -278,8 +278,8 @@ impl ChatCloudService for LocalChatServiceImpl { chat_id: &Uuid, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = read_chat(db, &chat_id)?; let rag_ids = deserialize_rag_ids(&row.rag_ids); let metadata = deserialize_chat_metadata::(&row.metadata); @@ -298,8 +298,8 @@ impl ChatCloudService for LocalChatServiceImpl { id: &Uuid, s: UpdateChatParams, ) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let mut db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let mut db = self.logged_user.get_sqlite_db(uid)?; let changeset = ChatTableChangeset { chat_id: id.to_string(), name: s.name, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 72bab514ec..1acb0846c9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,5 +1,6 @@ #![allow(unused_variables)] +use crate::af_cloud::define::LoggedUser; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab_entity::CollabType; @@ -10,9 +11,13 @@ use flowy_folder_pub::cloud::{ }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerFolderCloudServiceImpl; +pub(crate) struct LocalServerFolderCloudServiceImpl { + #[allow(dead_code)] + pub logged_user: Arc, +} #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 512ad90a22..bd6e930584 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,32 +1,38 @@ #![allow(unused_variables)] +use crate::af_cloud::define::LoggedUser; +use crate::local_server::uid::UserIDGenerator; use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; -use lazy_static::lazy_static; -use std::sync::Arc; -use tokio::sync::Mutex; -use uuid::Uuid; - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::uid::UserIDGenerator; +use flowy_ai_pub::cloud::billing_dto::WorkspaceUsageAndLimit; +use flowy_ai_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::{select_all_user_workspace, select_user_profile, select_user_workspace}; +use flowy_user_pub::sql::{ + select_all_user_workspace, select_user_profile, select_user_workspace, select_workspace_member, + select_workspace_setting, update_user_profile, update_workspace_setting, upsert_workspace_member, + upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, WorkspaceSettingsChangeset, + WorkspaceSettingsTable, +}; use flowy_user_pub::DEFAULT_USER_NAME; +use lazy_static::lazy_static; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use lib_infra::util::timestamp; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; lazy_static! { static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } pub(crate) struct LocalServerUserServiceImpl { - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -121,26 +127,30 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - async fn update_user(&self, _params: UpdateUserProfileParams) -> Result<(), FlowyError> { + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let changeset = UserTableChangeset::new(params); + update_user_profile(&mut conn, changeset)?; Ok(()) } async fn get_user_profile(&self, uid: i64) -> Result { - let conn = self.user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, conn)?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, &mut conn)?; Ok(profile) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { - let uid = self.user.user_id()?; - let mut conn = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; Ok(UserWorkspace::from(workspace)) } async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { - let conn = self.user.get_sqlite_db(uid)?; + let conn = self.logged_user.get_sqlite_db(uid)?; let workspaces = select_all_user_workspace(uid, conn)?; Ok(workspaces) } @@ -198,4 +208,117 @@ impl UserCloudService for LocalServerUserServiceImpl { ) -> Result<(), FlowyError> { Ok(()) } + + async fn get_workspace_member_info( + &self, + workspace_id: &Uuid, + uid: i64, + ) -> Result { + // For local server, only current user is the member + let conn = self.logged_user.get_sqlite_db(uid)?; + let result = select_workspace_member(conn, &workspace_id.to_string(), uid); + + match result { + Ok(row) => Ok(WorkspaceMember::from(row)), + Err(err) => { + if err.is_record_not_found() { + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, &mut conn)?; + let row = WorkspaceMemberTable { + email: profile.email.to_string(), + role: 0, + name: profile.name.to_string(), + avatar_url: Some(profile.icon_url), + uid, + workspace_id: workspace_id.to_string(), + updated_at: Default::default(), + }; + + let member = WorkspaceMember::from(row.clone()); + upsert_workspace_member(&mut conn, row)?; + Ok(member) + } else { + Err(err) + } + }, + } + } + + async fn get_workspace_usage( + &self, + workspace_id: &Uuid, + ) -> Result { + Ok(WorkspaceUsageAndLimit { + member_count: 1, + member_count_limit: 1, + storage_bytes: i64::MAX, + storage_bytes_limit: i64::MAX, + storage_bytes_unlimited: true, + single_upload_limit: i64::MAX, + single_upload_unlimited: true, + ai_responses_count: i64::MAX, + ai_responses_count_limit: i64::MAX, + ai_image_responses_count: i64::MAX, + ai_image_responses_count_limit: 0, + local_ai: true, + ai_responses_unlimited: true, + }) + } + + async fn get_workspace_setting( + &self, + workspace_id: &Uuid, + ) -> Result { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + + // By default, workspace setting is existed in local server + let result = select_workspace_setting(&mut conn, &workspace_id.to_string()); + match result { + Ok(row) => Ok(AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model, + }), + Err(err) => { + if err.is_record_not_found() { + let row = WorkspaceSettingsTable { + id: workspace_id.to_string(), + disable_search_indexing: false, + ai_model: "".to_string(), + }; + let setting = AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model.clone(), + }; + upsert_workspace_setting(&mut conn, row)?; + Ok(setting) + } else { + Err(err) + } + }, + } + } + + async fn update_workspace_setting( + &self, + workspace_id: &Uuid, + workspace_settings: AFWorkspaceSettingsChange, + ) -> Result { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + + let changeset = WorkspaceSettingsChangeset { + id: workspace_id.to_string(), + disable_search_indexing: workspace_settings.disable_search_indexing, + ai_model: workspace_settings.ai_model, + }; + + update_workspace_setting(&mut conn, changeset)?; + let row = select_workspace_setting(&mut conn, &workspace_id.to_string())?; + + Ok(AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model, + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 8ce0d86221..8ddcb2d76c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -17,15 +17,15 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + logged_user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(logged_user: Arc, local_ai: Arc) -> Self { Self { - user, + logged_user, local_ai, stop_tx: Default::default(), } @@ -42,12 +42,14 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { Arc::new(LocalServerUserServiceImpl { - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } fn folder_service(&self) -> Arc { - Arc::new(LocalServerFolderCloudServiceImpl) + Arc::new(LocalServerFolderCloudServiceImpl { + logged_user: self.logged_user.clone(), + }) } fn database_service(&self) -> Arc { @@ -64,7 +66,7 @@ impl AppFlowyServer for LocalServer { fn chat_service(&self) -> Arc { Arc::new(LocalChatServiceImpl { - user: self.user.clone(), + logged_user: self.logged_user.clone(), local_ai: self.local_ai.clone(), }) } diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 249ff9136d..7e38f423cc 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -1,10 +1,10 @@ use client_api::ClientConfiguration; +use collab_plugins::CollabKVDB; +use flowy_error::{FlowyError, FlowyResult}; use semver::Version; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::Arc; - -use flowy_error::{FlowyError, FlowyResult}; +use std::sync::{Arc, Weak}; use uuid::Uuid; use crate::setup_log; @@ -61,6 +61,10 @@ impl LoggedUser for FakeServerUserImpl { todo!() } + fn get_collab_db(&self, _uid: i64) -> Result, FlowyError> { + todo!() + } + fn application_root_dir(&self) -> Result { todo!() } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 0964d80472..b15299b194 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -285,10 +285,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, uid: i64, - ) -> Result { - Err(FlowyError::not_support()) - } - + ) -> Result; /// Get all subscriptions for all workspaces for a user (email) async fn get_workspace_subscriptions( &self, @@ -323,9 +320,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_usage( &self, workspace_id: &Uuid, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; async fn get_billing_portal_url(&self) -> Result { Err(FlowyError::not_support()) @@ -337,27 +332,23 @@ pub trait UserCloudService: Send + Sync + 'static { plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } async fn get_subscription_plan_details(&self) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn get_workspace_setting( &self, workspace_id: &Uuid, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; async fn update_workspace_setting( &self, workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs index bc73a8f34c..58ca65e732 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs @@ -1,10 +1,11 @@ +use crate::entities::{Role, WorkspaceMember}; use diesel::{insert_into, RunQueryDsl}; use flowy_error::FlowyResult; use flowy_sqlite::schema::workspace_members_table; use flowy_sqlite::schema::workspace_members_table::dsl; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; -#[derive(Queryable, Insertable, AsChangeset, Debug)] +#[derive(Queryable, Insertable, AsChangeset, Debug, Clone)] #[diesel(table_name = workspace_members_table)] #[diesel(primary_key(email, workspace_id))] pub struct WorkspaceMemberTable { @@ -17,8 +18,19 @@ pub struct WorkspaceMemberTable { pub updated_at: chrono::NaiveDateTime, } +impl From for WorkspaceMember { + fn from(value: WorkspaceMemberTable) -> Self { + Self { + email: value.email, + role: Role::from(value.role), + name: value.name, + avatar_url: value.avatar_url, + } + } +} + pub fn upsert_workspace_member>( - mut conn: DBConnection, + conn: &mut SqliteConnection, member: T, ) -> FlowyResult<()> { let member = member.into(); @@ -31,7 +43,7 @@ pub fn upsert_workspace_member>( )) .do_update() .set(&member) - .execute(&mut conn)?; + .execute(conn)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 5a910888a8..4cdf26520e 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -92,10 +92,24 @@ impl From for UserTableChangeset { } } -pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result { +pub fn update_user_profile( + conn: &mut SqliteConnection, + changeset: UserTableChangeset, +) -> Result<(), FlowyError> { + let user_id = changeset.id.clone(); + update(user_table::dsl::user_table.filter(user_table::id.eq(&user_id))) + .set(changeset) + .execute(conn)?; + Ok(()) +} + +pub fn select_user_profile( + uid: i64, + conn: &mut SqliteConnection, +) -> Result { let user: UserProfile = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) - .first::(&mut *conn) + .first::(conn) .map_err(|err| { FlowyError::record_not_found().with_context(format!( "Can't find the user profile for user id: {}, error: {:?}", diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs index 667d1f0ca0..7eeafaf1e4 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs @@ -45,7 +45,7 @@ pub fn update_workspace_setting( /// Upserts a workspace setting into the database. pub fn upsert_workspace_setting( - conn: &mut DBConnection, + conn: &mut SqliteConnection, settings: WorkspaceSettingsTable, ) -> Result<(), FlowyError> { diesel::insert_into(dsl::workspace_setting_table) @@ -62,11 +62,11 @@ pub fn upsert_workspace_setting( /// Selects a workspace setting by id from the database. pub fn select_workspace_setting( - conn: &mut DBConnection, - id: &str, + conn: &mut SqliteConnection, + workspace_id: &str, ) -> Result { let setting = dsl::workspace_setting_table - .filter(workspace_setting_table::id.eq(id)) + .filter(workspace_setting_table::id.eq(workspace_id)) .first::(conn)?; Ok(setting) } diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 2db5f418de..a4c7d3bd1d 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -103,7 +103,7 @@ pub(crate) fn prepare_import( ); let imported_user = select_user_profile( imported_session.user_id, - imported_sqlite_db.get_connection()?, + &mut *imported_sqlite_db.get_connection()?, )?; run_collab_data_migration( diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 138ad95819..3280370c88 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -126,8 +126,8 @@ impl UserDB { pool: &Arc, uid: i64, ) -> Result { - let conn = pool.get()?; - let profile = select_user_profile(uid, conn)?; + let mut conn = pool.get()?; + let profile = select_user_profile(uid, &mut conn)?; Ok(profile) } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 4299f0823b..3b568f976e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,7 +1,7 @@ use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::{internal_error, FlowyResult}; +use flowy_error::FlowyResult; use arc_swap::ArcSwapOption; use collab::lock::RwLock; @@ -506,8 +506,12 @@ impl UserManager { self.db_connection(session.user_id)?, changeset, )?; + self + .cloud_service + .get_user_service()? + .update_user(params) + .await?; - self.update_user(params).await?; Ok(()) } @@ -539,7 +543,8 @@ impl UserManager { /// Fetches the user profile for the given user ID. pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { - select_user_profile(uid, self.db_connection(uid)?) + let mut conn = self.db_connection(uid)?; + select_user_profile(uid, &mut conn) } #[tracing::instrument(level = "info", skip_all, err)] @@ -625,14 +630,6 @@ impl UserManager { Ok(None) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { - let server = self.cloud_service.get_user_service()?; - tokio::spawn(async move { server.update_user(params).await }) - .await - .map_err(internal_error)??; - Ok(()) - } - async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { let conn = self.db_connection(uid)?; upsert_user(user, conn)?; @@ -816,7 +813,7 @@ pub fn upsert_user_profile_change( "Update user profile with changeset: {:?}", changeset ); - diesel_update_table!(user_table, changeset, &mut *conn); + update_user_profile(&mut conn, changeset)?; let user: UserProfile = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) .first::(&mut *conn)? diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index b68643c83b..425b2351b0 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -668,8 +668,8 @@ impl UserManager { updated_at: Utc::now().naive_utc(), }; - let db = self.authenticate_user.get_sqlite_connection(uid)?; - upsert_workspace_member(db, record)?; + let mut db = self.authenticate_user.get_sqlite_connection(uid)?; + upsert_workspace_member(&mut db, record)?; Ok(member) } From 791a79a2341b3744727e1a8378b97f2701a39902 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 17:29:27 +0800 Subject: [PATCH 380/384] chore: impl local unspport --- .../workspace/_sidebar_workspace_menu.dart | 2 +- frontend/rust-lib/Cargo.lock | 16 +++---- frontend/rust-lib/Cargo.toml | 16 +++---- .../src/local_server/impls/database.rs | 42 +++++++++-------- .../src/local_server/impls/folder.rs | 29 +++++++++++- .../flowy-server/src/local_server/mod.rs | 1 + .../flowy-server/src/local_server/server.rs | 4 +- .../flowy-server/src/local_server/util.rs | 45 +++++++++++++++++++ 8 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 frontend/rust-lib/flowy-server/src/local_server/util.rs diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 04eb701a66..4ff5ccbf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index dcac892c6a..4a3fae985a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "bytes", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-recursion", @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-stream", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 61259d1e7b..1561c7ea7d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -144,14 +144,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 46b0cdd649..ad1184a09a 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,16 +1,18 @@ #![allow(unused_variables)] + +use crate::af_cloud::define::LoggedUser; +use crate::local_server::util::default_encode_collab_for_collab_type; use collab::entity::EncodedCollab; -use collab_database::database::default_database_data; -use collab_database::workspace_database::default_workspace_database_data; -use collab_document::document_data::default_document_collab_data; use collab_entity::CollabType; -use collab_user::core::default_user_awareness_data; use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; -use flowy_error::FlowyError; +use flowy_error::{ErrorCode, FlowyError}; use lib_infra::async_trait::async_trait; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerDatabaseCloudServiceImpl(); +pub(crate) struct LocalServerDatabaseCloudServiceImpl { + pub logged_user: Arc, +} #[async_trait] impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { @@ -18,24 +20,20 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { &self, object_id: &Uuid, collab_type: CollabType, - workspace_id: &Uuid, + _workspace_id: &Uuid, // underscore to silence “unused” warning ) -> Result, FlowyError> { + let uid = self.logged_user.user_id()?; let object_id = object_id.to_string(); - match collab_type { - CollabType::Document => { - let encode_collab = default_document_collab_data(&object_id)?; - Ok(Some(encode_collab)) - }, - CollabType::Database => default_database_data(&object_id) - .await - .map(Some) - .map_err(Into::into), - CollabType::WorkspaceDatabase => Ok(Some(default_workspace_database_data(&object_id))), - CollabType::Folder => Ok(None), - CollabType::DatabaseRow => Ok(None), - CollabType::UserAwareness => Ok(Some(default_user_awareness_data(&object_id))), - CollabType::Unknown => Ok(None), - } + default_encode_collab_for_collab_type(uid, &object_id, collab_type) + .await + .map(Some) + .or_else(|err| { + if matches!(err.code, ErrorCode::NotSupportYet) { + Ok(None) + } else { + Err(err) + } + }) } async fn create_database_encode_collab( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 1acb0846c9..7f4720de99 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,9 +1,14 @@ #![allow(unused_variables)] use crate::af_cloud::define::LoggedUser; +use crate::local_server::util::default_encode_collab_for_collab_type; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; +use collab::core::origin::CollabOrigin; +use collab::preclude::Collab; use collab_entity::CollabType; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, @@ -61,7 +66,29 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { collab_type: CollabType, object_id: &Uuid, ) -> Result, FlowyError> { - Err(FlowyError::local_version_not_support()) + let object_id = object_id.to_string(); + let workspace_id = workspace_id.to_string(); + let collab_db = self.logged_user.get_collab_db(uid)?.upgrade().unwrap(); + let read_txn = collab_db.read_txn(); + let is_exist = read_txn.is_exist(uid, &workspace_id.to_string(), &object_id.to_string()); + if is_exist { + // load doc + let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); + read_txn.load_doc(uid, &workspace_id, &object_id, collab.doc())?; + let data = collab.encode_collab_v1(|c| { + collab_type + .validate_require_data(c) + .map_err(|err| FlowyError::invalid_data().with_context(err))?; + Ok::<_, FlowyError>(()) + })?; + Ok(data.doc_state.to_vec()) + } else { + let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; + drop(read_txn); + + // create default folder doc + Err(FlowyError::local_version_not_support()) + } } async fn batch_create_folder_collab_objects( diff --git a/frontend/rust-lib/flowy-server/src/local_server/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/mod.rs index 6e67356fd9..2b9fe07250 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/mod.rs @@ -3,3 +3,4 @@ pub use server::*; pub mod impls; mod server; pub(crate) mod uid; +mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 8ddcb2d76c..9ce19f5df6 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -53,7 +53,9 @@ impl AppFlowyServer for LocalServer { } fn database_service(&self) -> Arc { - Arc::new(LocalServerDatabaseCloudServiceImpl()) + Arc::new(LocalServerDatabaseCloudServiceImpl { + logged_user: self.logged_user.clone(), + }) } fn database_ai_service(&self) -> Option> { diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs new file mode 100644 index 0000000000..bd00212afb --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/util.rs @@ -0,0 +1,45 @@ +use collab::core::origin::CollabOrigin; +use collab::entity::EncodedCollab; +use collab::preclude::Collab; +use collab_database::database::default_database_data; +use collab_database::workspace_database::default_workspace_database_data; +use collab_document::document_data::default_document_collab_data; +use collab_entity::CollabType; +use collab_user::core::default_user_awareness_data; +use flowy_error::{FlowyError, FlowyResult}; + +pub async fn default_encode_collab_for_collab_type( + _uid: i64, + object_id: &str, + collab_type: CollabType, +) -> FlowyResult { + match collab_type { + CollabType::Document => { + let encode_collab = default_document_collab_data(object_id)?; + Ok(encode_collab) + }, + CollabType::Database => default_database_data(object_id).await.map_err(Into::into), + CollabType::WorkspaceDatabase => Ok(default_workspace_database_data(object_id)), + CollabType::Folder => { + // let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); + // let workspace = Workspace::new(object_id.to_string(), "".to_string(), uid); + // let folder_data = FolderData::new(workspace); + // let folder = Folder::create(uid, collab, None, folder_data); + // let data = folder.encode_collab_v1(|c| { + // collab_type + // .validate_require_data(c) + // .map_err(|err| FlowyError::invalid_data().with_context(err))?; + // Ok::<_, FlowyError>(()) + // })?; + // Ok(data) + Err(FlowyError::not_support()) + }, + CollabType::DatabaseRow => Err(FlowyError::not_support()), + CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), + CollabType::Unknown => { + let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); + let data = collab.encode_collab_v1(|_| Ok::<_, FlowyError>(()))?; + Ok(data) + }, + } +} From 92d5690bba8e417ee11aa34c8a72b36886e80134 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 18:07:02 +0800 Subject: [PATCH 381/384] chore: pass folder init data --- frontend/rust-lib/Cargo.lock | 1 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 1 + .../flowy-core/src/user_state_callback.rs | 117 ++++++++++++------ frontend/rust-lib/flowy-folder/Cargo.toml | 1 + frontend/rust-lib/flowy-folder/src/manager.rs | 54 +++----- .../src/local_server/impls/folder.rs | 4 +- .../rust-lib/flowy-storage/src/manager.rs | 2 +- frontend/rust-lib/flowy-user/src/event_map.rs | 7 +- .../user_manager/manager_user_workspace.rs | 2 +- 10 files changed, 106 insertions(+), 85 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4a3fae985a..51a3f1a3b2 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2835,6 +2835,7 @@ dependencies = [ "flowy-notification", "flowy-search-pub", "flowy-sqlite", + "flowy-user-pub", "futures", "lazy_static", "lib-dispatch", diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 399a8d2d5d..9055341b99 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -131,7 +131,7 @@ impl AIManager { #[instrument(skip_all, err)] pub async fn initialize_after_open_workspace( &self, - _workspace_id: &str, + _workspace_id: &Uuid, ) -> Result<(), FlowyError> { let local_ai = self.local_ai.clone(); tokio::spawn(async move { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 893a010a18..c2800bd73b 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -253,6 +253,7 @@ impl AppFlowyCore { .await; let user_status_callback = UserStatusCallbackImpl { + user_manager: user_manager.clone(), collab_builder, folder_manager: folder_manager.clone(), database_manager: database_manager.clone(), diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index e43002d709..8db4e3f926 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -4,23 +4,27 @@ use anyhow::Context; use client_api::entity::billing_dto::SubscriptionPlan; use tracing::{error, event, info}; +use crate::server_layer::ServerProvider; use collab_entity::CollabType; use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; +use flowy_user::user_manager::UserManager; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; - -use crate::server_layer::ServerProvider; +use uuid::Uuid; pub(crate) struct UserStatusCallbackImpl { + pub(crate) user_manager: Arc, pub(crate) collab_builder: Arc, pub(crate) folder_manager: Arc, pub(crate) database_manager: Arc, @@ -42,6 +46,60 @@ impl UserStatusCallbackImpl { } }); } + + async fn folder_init_data_source( + &self, + user_id: i64, + workspace_id: &Uuid, + auth_type: &AuthType, + ) -> FlowyResult { + let is_exist = self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)?; + if is_exist { + Ok(FolderInitDataSource::LocalDisk { + create_if_not_exist: false, + }) + } else { + let data_source = match self + .folder_manager + .cloud_service + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) + .await + { + Ok(doc_state) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + }, + Err(err) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => { + return Err(err); + }, + }, + }; + Ok(data_source) + } + } + + fn is_object_exist_on_disk( + &self, + user_id: i64, + workspace_id: &Uuid, + object_id: &Uuid, + ) -> FlowyResult { + let db = self + .user_manager + .get_collab_db(user_id)? + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Collab db is not initialized"))?; + let read = db.read_txn(); + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); + Ok(read.is_exist(user_id, &workspace_id, &object_id)) + } } #[async_trait] @@ -101,9 +159,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); + let workspace_id = user_workspace.workspace_id()?; + let data_source = self + .folder_init_data_source(user_id, &workspace_id, auth_type) + .await?; self .folder_manager - .initialize_after_sign_in(user_id) + .initialize_after_sign_in(user_id, data_source) .await?; self .database_manager @@ -135,37 +197,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { device_id ); let workspace_id = user_workspace.workspace_id()?; - - // In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace - // is automatically created for them. However, for users who sign up through Supabase, the creation - // of the default workspace relies on the client-side operation. This means that the process - // for initializing a default workspace differs depending on the sign-up method used. - let data_source = match self - .folder_manager - .cloud_service - .get_folder_doc_state( - &workspace_id, - user_profile.uid, - CollabType::Folder, - &workspace_id, - ) - .await - { - Ok(doc_state) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }, - Err(err) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => { - return Err(err); - }, - }, - }; + let data_source = self + .folder_init_data_source(user_profile.uid, &workspace_id, auth_type) + .await?; self .folder_manager @@ -204,12 +238,17 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn on_workspace_opened( &self, user_id: i64, - user_workspace: &UserWorkspace, + workspace_id: &Uuid, + _user_workspace: &UserWorkspace, auth_type: &AuthType, ) -> FlowyResult<()> { + let data_source = self + .folder_init_data_source(user_id, workspace_id, auth_type) + .await?; + self .folder_manager - .initialize_after_open_workspace(user_id) + .initialize_after_open_workspace(user_id, data_source) .await?; self .database_manager @@ -221,11 +260,11 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .ai_manager - .initialize_after_open_workspace(&user_workspace.id) + .initialize_after_open_workspace(workspace_id) .await?; self .storage_manager - .initialize_after_open_workspace(&user_workspace.id) + .initialize_after_open_workspace(workspace_id) .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 998fcb84f5..13b19e48b8 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -14,6 +14,7 @@ collab-plugins = { workspace = true } collab-integrate = { workspace = true } flowy-folder-pub = { workspace = true } flowy-search-pub = { workspace = true } +flowy-user-pub = { workspace = true } flowy-sqlite = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 8662c1e061..fd12c15fba 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -262,16 +262,17 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. - #[tracing::instrument(skip(self, user_id), err)] - pub async fn initialize_after_sign_in(&self, user_id: i64) -> FlowyResult<()> { + #[tracing::instrument(skip_all, err)] + pub async fn initialize_after_sign_in( + &self, + user_id: i64, + data_source: FolderInitDataSource, + ) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; - let object_id = &workspace_id; - - let is_exist = self - .user - .is_folder_exist_on_disk(user_id, &workspace_id) - .unwrap_or(false); - if is_exist { + if let Err(err) = self.initialize(user_id, &workspace_id, data_source).await { + // If failed to open folder with remote data, open from local disk. After open from the local + // disk. the data will be synced to the remote server. + error!("initialize folder with error {:?}, fallback local", err); self .initialize( user_id, @@ -281,39 +282,17 @@ impl FolderManager { }, ) .await?; - } else { - let folder_doc_state = self - .cloud_service - .get_folder_doc_state(&workspace_id, user_id, CollabType::Folder, object_id) - .await?; - if let Err(err) = self - .initialize( - user_id, - &workspace_id, - FolderInitDataSource::Cloud(folder_doc_state), - ) - .await - { - // If failed to open folder with remote data, open from local disk. After open from the local - // disk. the data will be synced to the remote server. - error!("initialize folder with error {:?}, fallback local", err); - self - .initialize( - user_id, - &workspace_id, - FolderInitDataSource::LocalDisk { - create_if_not_exist: false, - }, - ) - .await?; - } } Ok(()) } - pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { - self.initialize_after_sign_in(uid).await + pub async fn initialize_after_open_workspace( + &self, + uid: i64, + data_source: FolderInitDataSource, + ) -> FlowyResult<()> { + self.initialize_after_sign_in(uid, data_source).await } /// Initialize the folder for the new user. @@ -2139,6 +2118,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde } #[allow(clippy::large_enum_variant)] +#[derive(Debug)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 7f4720de99..483ca3d100 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -85,9 +85,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { } else { let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; drop(read_txn); - - // create default folder doc - Err(FlowyError::local_version_not_support()) + Ok(data.doc_state.to_vec()) } } diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index 619dc47f90..0dd729b087 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -181,7 +181,7 @@ impl StorageManager { } } - pub async fn initialize_after_open_workspace(&self, workspace_id: &str) { + pub async fn initialize_after_open_workspace(&self, workspace_id: &Uuid) { self.enable_storage_write_access(); if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 0b489e6f28..fbee0a96d9 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,13 +1,13 @@ use client_api::entity::billing_dto::SubscriptionPlan; -use std::sync::Weak; -use strum_macros::Display; - use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::async_trait::async_trait; +use std::sync::Weak; +use strum_macros::Display; +use uuid::Uuid; use crate::event_handler::*; use crate::user_manager::UserManager; @@ -323,6 +323,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { async fn on_workspace_opened( &self, _user_id: i64, + _workspace_id: &Uuid, _user_workspace: &UserWorkspace, _auth_type: &AuthType, ) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 425b2351b0..34d0ece483 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -195,7 +195,7 @@ impl UserManager { .user_status_callback .read() .await - .on_workspace_opened(uid, &user_workspace, &user_profile.auth_type) + .on_workspace_opened(uid, workspace_id, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); From 2ee786f3512a8d631ac084596fd1716dd5a147c0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 19:29:56 +0800 Subject: [PATCH 382/384] chore: update logs --- .../folder_deps/folder_deps_chat_impl.rs | 8 +-- .../folder_deps/folder_deps_database_impl.rs | 4 +- .../flowy-core/src/user_state_callback.rs | 56 ++++++++++--------- .../flowy-server/src/af_cloud/impls/chat.rs | 4 +- .../af_cloud/impls/user/cloud_service_impl.rs | 31 ++-------- .../src/local_server/impls/chat.rs | 6 +- .../src/local_server/impls/user.rs | 2 +- .../flowy-server/src/local_server/util.rs | 6 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 12 +--- .../user_manager/manager_user_workspace.rs | 4 +- 10 files changed, 55 insertions(+), 78 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs index fc7a861cb8..e2791827ee 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs @@ -31,7 +31,7 @@ impl FolderOperationHandler for ChatFolderOperation { } async fn duplicate_view(&self, _view_id: &Uuid) -> Result { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Duplicate view")) } async fn create_view_with_view_data( @@ -39,7 +39,7 @@ impl FolderOperationHandler for ChatFolderOperation { _user_id: i64, _params: CreateViewParams, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Can't create view")) } async fn create_default_view( @@ -65,7 +65,7 @@ impl FolderOperationHandler for ChatFolderOperation { _import_type: ImportType, _bytes: Vec, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("import from data")) } async fn import_from_file_path( @@ -74,6 +74,6 @@ impl FolderOperationHandler for ChatFolderOperation { _name: &str, _path: String, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("import file from path")) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs index d98e32f67d..edc40c6d5b 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs @@ -198,7 +198,9 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar, ViewLayoutPB::Grid => DatabaseLayoutPB::Grid, ViewLayoutPB::Document | ViewLayoutPB::Chat => { - return Err(FlowyError::not_support()); + return Err( + FlowyError::invalid_data().with_context("Can't handle document layout type"), + ); }, }; let name = params.name.to_string(); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 8db4e3f926..3be1bf15ed 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -53,35 +53,17 @@ impl UserStatusCallbackImpl { workspace_id: &Uuid, auth_type: &AuthType, ) -> FlowyResult { - let is_exist = self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)?; - if is_exist { - Ok(FolderInitDataSource::LocalDisk { + if self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)? { + return Ok(FolderInitDataSource::LocalDisk { create_if_not_exist: false, - }) - } else { - let data_source = match self - .folder_manager - .cloud_service - .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) - .await - { - Ok(doc_state) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }, - Err(err) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => { - return Err(err); - }, - }, - }; - Ok(data_source) + }); } + let doc_state_result = self + .folder_manager + .cloud_service + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) + .await; + resolve_data_source(auth_type, doc_state_result) } fn is_object_exist_on_disk( @@ -296,3 +278,23 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } } + +fn resolve_data_source( + auth_type: &AuthType, + doc_state_result: Result, FlowyError>, +) -> FlowyResult { + match doc_state_result { + Ok(doc_state) => Ok(match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + }), + Err(err) => match auth_type { + AuthType::Local => Ok(FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }), + AuthType::AppFlowyCloud => Err(err), + }, + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 14a26078f5..6086f7084b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -217,10 +217,10 @@ where chat_id: &Uuid, metadata: Option>, ) -> Result<(), FlowyError> { - return Err( + Err( FlowyError::not_support() .with_context("indexing file with appflowy cloud is not suppotred yet"), - ); + ) } async fn get_chat_settings( diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index e0f81a62e4..d6260d9e09 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -13,8 +13,8 @@ use client_api::entity::workspace_dto::{ WorkspaceMemberInvitation, }; use client_api::entity::{ - AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, - AuthProvider, CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, + AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, AuthProvider, + CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; @@ -341,18 +341,6 @@ where Ok(members) } - async fn get_workspace_member( - &self, - workspace_id: Uuid, - uid: i64, - ) -> Result { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let query = QueryWorkspaceMember { workspace_id, uid }; - let member = client.get_workspace_member(query).await?; - Ok(from_af_workspace_member(member)) - } - #[instrument(level = "debug", skip_all)] async fn get_user_awareness_doc_state( &self, @@ -452,7 +440,7 @@ where Ok(payment_link) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, @@ -464,17 +452,8 @@ where uid, }; let member = client.get_workspace_member(params).await?; - let role = match member.role { - AFRole::Owner => Role::Owner, - AFRole::Member => Role::Member, - AFRole::Guest => Role::Guest, - }; - Ok(WorkspaceMember { - email: member.email, - role, - name: member.name, - avatar_url: member.avatar_url, - }) + + Ok(from_af_workspace_member(member)) } async fn get_workspace_subscriptions( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 3af87eb18e..845b6dec1c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -8,7 +8,7 @@ use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, - ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, + ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, DEFAULT_AI_MODEL_NAME, }; use flowy_ai_pub::persistence::{ deserialize_chat_metadata, deserialize_rag_ids, read_chat, @@ -313,11 +313,11 @@ impl ChatCloudService for LocalChatServiceImpl { } async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(ModelList { models: vec![] }) } async fn get_workspace_default_model(&self, _workspace_id: &Uuid) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(DEFAULT_AI_MODEL_NAME.to_string()) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index bd6e930584..0023defb41 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -209,7 +209,7 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs index bd00212afb..378ccee6a2 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/util.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/util.rs @@ -32,9 +32,11 @@ pub async fn default_encode_collab_for_collab_type( // Ok::<_, FlowyError>(()) // })?; // Ok(data) - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Can not create default folder")) + }, + CollabType::DatabaseRow => { + Err(FlowyError::not_support().with_context("Can not create default database row")) }, - CollabType::DatabaseRow => Err(FlowyError::not_support()), CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), CollabType::Unknown => { let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index b15299b194..e58c626532 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -132,7 +132,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Delete an account and all the data associated with the account async fn delete_account(&self) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } /// Generate a sign in url for the user with the given email @@ -234,14 +234,6 @@ pub trait UserCloudService: Send + Sync + 'static { Ok(vec![]) } - async fn get_workspace_member( - &self, - workspace_id: Uuid, - uid: i64, - ) -> Result { - Err(FlowyError::not_support()) - } - async fn get_user_awareness_doc_state( &self, uid: i64, @@ -281,7 +273,7 @@ pub trait UserCloudService: Send + Sync + 'static { Err(FlowyError::not_support()) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 34d0ece483..f74cf45ef7 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -377,7 +377,7 @@ impl UserManager { let member = self .cloud_service .get_user_service()? - .get_workspace_member(workspace_id, uid) + .get_workspace_member(&workspace_id, uid) .await?; Ok(member) } @@ -655,7 +655,7 @@ impl UserManager { let member = self .cloud_service .get_user_service()? - .get_workspace_member_info(workspace_id, uid) + .get_workspace_member(workspace_id, uid) .await?; let record = WorkspaceMemberTable { From cf46213e0019b1b1228479598b1e46b5d7d1d71c Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:34:05 +0800 Subject: [PATCH 383/384] chore: Update frontend/rust-lib/flowy-folder/src/manager.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- frontend/rust-lib/flowy-folder/src/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index fd12c15fba..a12959b803 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -272,7 +272,7 @@ impl FolderManager { if let Err(err) = self.initialize(user_id, &workspace_id, data_source).await { // If failed to open folder with remote data, open from local disk. After open from the local // disk. the data will be synced to the remote server. - error!("initialize folder with error {:?}, fallback local", err); + error!("initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", user_id, workspace_id, err); self .initialize( user_id, From c6010a6734a7b7c61d7ce306be96735f00a91f0b Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 19:51:12 +0800 Subject: [PATCH 384/384] chore: fmt --- frontend/rust-lib/flowy-folder/src/manager.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index a12959b803..8e228191c4 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -272,7 +272,10 @@ impl FolderManager { if let Err(err) = self.initialize(user_id, &workspace_id, data_source).await { // If failed to open folder with remote data, open from local disk. After open from the local // disk. the data will be synced to the remote server. - error!("initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", user_id, workspace_id, err); + error!( + "initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", + user_id, workspace_id, err + ); self .initialize( user_id,