From 4ff71b5dcef2089f4b15c3b4bbf0aa2df0fe5564 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 9 Mar 2025 23:32:42 +0800 Subject: [PATCH 001/296] 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 002/296] 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 003/296] 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 004/296] 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 005/296] 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 006/296] 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 007/296] 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 008/296] 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 009/296] 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 010/296] 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 011/296] 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 012/296] 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 013/296] 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 014/296] 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 015/296] 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 016/296] 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 017/296] 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 018/296] 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 019/296] 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 020/296] 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 021/296] 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 022/296] 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 023/296] 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 024/296] 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 025/296] 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 026/296] 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 027/296] 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 028/296] 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 029/296] 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 030/296] 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 031/296] 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 032/296] 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 033/296] 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 034/296] 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 035/296] 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 036/296] 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 037/296] 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 038/296] 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 039/296] 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 040/296] 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 041/296] 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 042/296] 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 043/296] 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 044/296] 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 045/296] 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 046/296] 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 047/296] 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 048/296] 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 049/296] 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 050/296] 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 051/296] 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 052/296] 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 053/296] 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 054/296] 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 055/296] 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 056/296] 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 057/296] 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 058/296] 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 059/296] 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 060/296] 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 061/296] 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 062/296] 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 063/296] 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 064/296] 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 065/296] 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 066/296] 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 067/296] 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 068/296] 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 069/296] 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 070/296] 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 071/296] 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 072/296] 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 073/296] 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 074/296] 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 075/296] 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 076/296] 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 077/296] 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 078/296] 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 079/296] 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 080/296] 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 081/296] 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 082/296] 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 083/296] 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 084/296] 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 085/296] 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 086/296] 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 087/296] 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 088/296] 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 089/296] 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 090/296] 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 091/296] 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 092/296] 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 093/296] 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 094/296] 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 095/296] 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 096/296] 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 097/296] 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 098/296] 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 099/296] 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 100/296] 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 101/296] 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 102/296] 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 103/296] 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 104/296] 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 105/296] 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 106/296] 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 107/296] 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 108/296] 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 109/296] 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 110/296] 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 111/296] 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 112/296] 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 113/296] 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 114/296] 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 115/296] 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 116/296] 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 117/296] 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 118/296] 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 119/296] 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 120/296] 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 121/296] 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 122/296] 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 123/296] 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 124/296] 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 125/296] 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 126/296] 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 127/296] 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 128/296] 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 129/296] 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 130/296] 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 131/296] 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 132/296] =?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 133/296] 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 134/296] 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 135/296] 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 136/296] 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 137/296] 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 138/296] 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 139/296] 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 140/296] 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 141/296] 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 142/296] 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 143/296] 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 144/296] 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 145/296] 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 146/296] 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 147/296] 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 148/296] 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 149/296] 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 150/296] 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 151/296] 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 152/296] 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 153/296] 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 154/296] 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 155/296] 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 156/296] 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 157/296] 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 158/296] 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 159/296] 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 160/296] 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 161/296] 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 162/296] 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 163/296] 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 164/296] 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 165/296] 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 166/296] 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 167/296] 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 168/296] 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 169/296] 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 170/296] 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 171/296] 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 172/296] 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 173/296] 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 174/296] 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 175/296] 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 176/296] 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 177/296] 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 178/296] 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 179/296] 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 180/296] 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 181/296] 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 182/296] 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 183/296] =?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 184/296] 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 185/296] 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 186/296] =?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 187/296] 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 188/296] 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 189/296] 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 190/296] 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 191/296] 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 192/296] 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 193/296] 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 194/296] 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 195/296] 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 196/296] 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 197/296] 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 198/296] 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 199/296] 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 200/296] 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 201/296] 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 202/296] 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 203/296] 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 204/296] 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 205/296] 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 206/296] 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 207/296] 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 208/296] 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 209/296] 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 210/296] 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 211/296] 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 212/296] 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 213/296] 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 214/296] 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 215/296] 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 216/296] 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 217/296] 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 218/296] 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 219/296] 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 220/296] 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 221/296] 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 222/296] 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 223/296] 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 224/296] 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 225/296] 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 226/296] 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 227/296] 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 228/296] 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 229/296] 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 230/296] 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 231/296] 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 232/296] 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 233/296] 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 234/296] 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 235/296] 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 236/296] 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 237/296] 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 238/296] 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 239/296] 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 240/296] 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 241/296] 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 242/296] 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 243/296] 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 244/296] 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 245/296] 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 246/296] 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 247/296] 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 248/296] 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 249/296] 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 250/296] 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 251/296] 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 252/296] 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 253/296] 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 254/296] 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 255/296] 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 256/296] 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 257/296] 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 258/296] 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 259/296] 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 260/296] 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 261/296] 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 262/296] 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 263/296] 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 264/296] 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 265/296] 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 266/296] 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 267/296] 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 268/296] 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 269/296] 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 270/296] 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 271/296] 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 272/296] 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 273/296] 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 274/296] 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 275/296] 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 276/296] 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 277/296] 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 278/296] 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 279/296] 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 280/296] 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 281/296] 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 282/296] 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 283/296] 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 284/296] 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 285/296] 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 286/296] 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 287/296] 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 288/296] 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 289/296] 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 290/296] 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 291/296] 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 292/296] 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 293/296] 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 294/296] 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 295/296] 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 296/296] 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, );