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/CHANGELOG.md b/CHANGELOG.md
index 9db399773b..a5e7e268a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,35 @@
# 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
+- 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
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index 44337645fe..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.7"
+APPFLOWY_VERSION = "0.8.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
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/assets/fonts/.gitkeep b/frontend/appflowy_flutter/assets/fonts/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf
deleted file mode 100644
index 8f03a5c8f9..0000000000
Binary files a/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf and /dev/null differ
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/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/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart
index 3271070c74..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';
@@ -44,12 +44,12 @@ 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;
- expect(secondDocumentWidget.result.data, secondDocument);
+ .widget(find.byType(SearchResultCell).first) as SearchResultCell;
+ expect(secondDocumentWidget.item.displayName, secondDocument);
// Change search to "ViewOne"
await tester.enterText(searchFieldFinder, firstDocument);
@@ -57,9 +57,9 @@ 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;
- expect(firstDocumentWidget.result.data, firstDocument);
+ find.byType(SearchResultCell).first,
+ ) as SearchResultCell;
+ expect(firstDocumentWidget.item.displayName, firstDocument);
});
testWidgets('Displaying icons in search results', (tester) async {
@@ -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/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/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/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/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..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
@@ -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,20 @@ 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 +372,19 @@ 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:
@@ -469,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
@@ -521,7 +525,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 +562,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_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/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..39f8bfd4f6
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart
@@ -0,0 +1,453 @@
+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_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';
+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,
+ LinkEmbedConvertCommand 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, LinkEmbedConvertCommand.toMention);
+ 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, LinkEmbedConvertCommand.toURL);
+ 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, LinkEmbedConvertCommand.toBookmark);
+ final node = tester.editor.getNodeAtPath([0]);
+ checkBookmark(node, link);
+ });
+ });
+}
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/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/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart
index abfee17d8e..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
@@ -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';
@@ -175,5 +184,187 @@ 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', () {
+ 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 tester.editor.tapLineOfEditorAt(0);
+ 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/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart
index 554a6eecbf..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
@@ -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,110 @@ void main() {
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
});
});
+
+ group('insert emoji by colon', () {
+ 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(':${search ?? 'a'}');
+ 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: 's');
+
+ /// search for `smiling eyes`, IME is not working, use keyboard input
+ final searchText = [
+ 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, search: ' ');
+
+ /// emoji list is showing
+ final emojiHandler = find.byType(EmojiHandler);
+ 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/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/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/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/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/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/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..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,28 @@ 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;
+
+ 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_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart
new file mode 100644
index 0000000000..0bcc41da9b
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart
@@ -0,0 +1,181 @@
+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,
+ AIModelPB?,
+);
+
+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) {
+ 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) {
+ 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, AIModelPB?) getAvailableModels() {
+ final availableModels = _availableModels;
+ if (availableModels == null) {
+ return ([], null);
+ }
+ return (availableModels.models, availableModels.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"),
+ );
+ }
+}
+
+extension AiModelExtension on AIModelPB {
+ bool get isDefault {
+ return name == "Auto";
+ }
+
+ String get i18n {
+ return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
+ }
+}
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..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,14 +1,8 @@
import 'dart:async';
-import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/ai/service/ai_model_state_notifier.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,20 @@ part 'ai_prompt_input_bloc.freezed.dart';
class AIPromptInputBloc extends Bloc {
AIPromptInputBloc({
+ required String objectId,
required PredefinedFormat? predefinedFormat,
- }) : _listener = LocalAIStateListener(),
+ }) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),
super(AIPromptInputState.initial(predefinedFormat)) {
_dispatch();
_startListening();
_init();
}
- final LocalAIStateListener _listener;
+ final AIModelStateNotifier aiModelStateNotifier;
@override
Future close() async {
- await _listener.stop();
+ await aiModelStateNotifier.dispose();
return super.close();
}
@@ -38,29 +33,10 @@ 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,
),
@@ -128,24 +104,16 @@ class AIPromptInputBloc extends Bloc {
}
void _startListening() {
- _listener.start(
- stateCallback: (pluginState) {
- if (!isClosed) {
- add(AIPromptInputEvent.updateAIState(pluginState));
- }
+ aiModelStateNotifier.addListener(
+ onStateChanged: (aiType, editable, hintText) {
+ add(AIPromptInputEvent.updateAIState(aiType, editable, hintText));
},
);
}
void _init() {
- AIEventGetLocalAIState().send().fold(
- (localAIState) {
- if (!isClosed) {
- add(AIPromptInputEvent.updateAIState(localAIState));
- }
- },
- Log.error,
- );
+ final (aiType, hintText, isEditable) = aiModelStateNotifier.getState();
+ add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText));
}
Map consumeMetadata() {
@@ -164,8 +132,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(
@@ -188,7 +160,6 @@ class AIPromptInputState with _$AIPromptInputState {
required bool supportChatWithFile,
required bool showPredefinedFormats,
required PredefinedFormat? predefinedFormat,
- required LocalAIPB? localAIState,
required List attachedFiles,
required List mentionedPages,
required bool editable,
@@ -201,18 +172,9 @@ class AIPromptInputState with _$AIPromptInputState {
supportChatWithFile: false,
showPredefinedFormats: format != null,
predefinedFormat: format,
- localAIState: null,
attachedFiles: [],
mentionedPages: [],
editable: true,
hintText: '',
);
}
-
-enum AiType {
- cloud,
- local;
-
- bool get isCloud => this == cloud;
- bool get isLocal => this == 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 3c1d5c5a9d..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,
@@ -24,9 +29,12 @@ 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,
+ required void Function(LocalAIStreamingState state)
+ onLocalAIStreamingStateChange,
});
}
@@ -40,15 +48,20 @@ 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,
+ required void Function(LocalAIStreamingState state)
+ onLocalAIStreamingStateChange,
}) async {
final stream = AppFlowyCompletionStream(
onStart: onStart,
- onProcess: onProcess,
+ processMessage: processMessage,
+ processAssistMessage: processAssistMessage,
+ processError: onError,
+ onLocalAIStreamingStateChange: onLocalAIStreamingStateChange,
onEnd: onEnd,
- onError: onError,
);
final records = history.map((record) => record.toPB()).toList();
@@ -79,23 +92,30 @@ class AppFlowyAIService implements AIRepository {
abstract class CompletionStream {
CompletionStream({
required this.onStart,
- required this.onProcess,
+ required this.processMessage,
+ required this.processAssistMessage,
+ required this.processError,
+ required this.onLocalAIStreamingStateChange,
required this.onEnd,
- required this.onError,
});
final Future Function() onStart;
- final Future Function(String text) onProcess;
+ final Future Function(String text) processMessage;
+ final Future Function(String text) processAssistMessage;
+ final void Function(AIError error) processError;
+ final void Function(LocalAIStreamingState state)
+ onLocalAIStreamingStateChange;
final Future Function() onEnd;
- final void Function(AIError error) onError;
}
class AppFlowyCompletionStream extends CompletionStream {
AppFlowyCompletionStream({
required super.onStart,
- required super.onProcess,
+ required super.processMessage,
+ required super.processAssistMessage,
+ required super.processError,
required super.onEnd,
- required super.onError,
+ required super.onLocalAIStreamingStateChange,
}) {
_startListening();
}
@@ -109,51 +129,7 @@ class AppFlowyCompletionStream extends CompletionStream {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
- if (event == "AI_RESPONSE_LIMIT") {
- onError(
- AIError(
- message: LocaleKeys.ai_textLimitReachedDescription.tr(),
- code: AIErrorCode.aiResponseLimitExceeded,
- ),
- );
- }
-
- if (event == "AI_IMAGE_RESPONSE_LIMIT") {
- onError(
- AIError(
- message: LocaleKeys.ai_imageLimitReachedDescription.tr(),
- code: AIErrorCode.aiImageResponseLimitExceeded,
- ),
- );
- }
-
- if (event.startsWith("AI_MAX_REQUIRED:")) {
- final msg = event.substring(16);
- onError(
- AIError(
- message: msg,
- code: AIErrorCode.other,
- ),
- );
- }
-
- if (event.startsWith("start:")) {
- await onStart();
- }
-
- if (event.startsWith("data:")) {
- await onProcess(event.substring(5));
- }
-
- if (event.startsWith("finish:")) {
- await onEnd();
- }
-
- if (event.startsWith("error:")) {
- onError(
- AIError(message: event.substring(6), code: AIErrorCode.other),
- );
- }
+ await _handleEvent(event);
},
);
}
@@ -163,4 +139,66 @@ 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
+ 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/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart
new file mode 100644
index 0000000000..7ad52b9ec4
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart
@@ -0,0 +1,92 @@
+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:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'select_model_bloc.freezed.dart';
+
+class SelectModelBloc extends Bloc {
+ SelectModelBloc({
+ required AIModelStateNotifier aiModelStateNotifier,
+ }) : _aiModelStateNotifier = aiModelStateNotifier,
+ super(SelectModelState.initial(aiModelStateNotifier)) {
+ on(
+ (event, emit) {
+ event.when(
+ selectModel: (model) {
+ AIEventUpdateSelectedModel(
+ UpdateSelectedModelPB(
+ source: _aiModelStateNotifier.objectId,
+ selectedModel: model,
+ ),
+ ).send();
+
+ emit(state.copyWith(selectedModel: model));
+ },
+ didLoadModels: (models, selectedModel) {
+ emit(
+ SelectModelState(
+ models: models,
+ selectedModel: selectedModel,
+ ),
+ );
+ },
+ );
+ },
+ );
+
+ _aiModelStateNotifier.addListener(
+ onAvailableModelsChanged: _onAvailableModelsChanged,
+ );
+ }
+
+ final AIModelStateNotifier _aiModelStateNotifier;
+
+ @override
+ Future close() async {
+ _aiModelStateNotifier.removeListener(
+ onAvailableModelsChanged: _onAvailableModelsChanged,
+ );
+ await super.close();
+ }
+
+ void _onAvailableModelsChanged(
+ List models,
+ AIModelPB? selectedModel,
+ ) {
+ if (!isClosed) {
+ add(SelectModelEvent.didLoadModels(models, selectedModel));
+ }
+ }
+}
+
+@freezed
+class SelectModelEvent with _$SelectModelEvent {
+ const factory SelectModelEvent.selectModel(
+ AIModelPB model,
+ ) = _SelectModel;
+
+ const factory SelectModelEvent.didLoadModels(
+ List models,
+ AIModelPB? selectedModel,
+ ) = _DidLoadModels;
+}
+
+@freezed
+class SelectModelState with _$SelectModelState {
+ const factory SelectModelState({
+ required List models,
+ required AIModelPB? selectedModel,
+ }) = _SelectModelState;
+
+ factory SelectModelState.initial(AIModelStateNotifier notifier) {
+ final (models, selectedModel) = notifier.getAvailableModels();
+ return SelectModelState(
+ models: models,
+ selectedModel: selectedModel,
+ );
+ }
+}
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..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
@@ -17,20 +17,26 @@ class DesktopPromptInput extends StatefulWidget {
const DesktopPromptInput({
super.key,
required this.isStreaming,
+ required this.textController,
required this.onStopStreaming,
required this.onSubmitted,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
this.hideDecoration = false,
+ this.hideFormats = false,
+ this.extraBottomActionButton,
});
final bool isStreaming;
+ final TextEditingController textController;
final void Function() onStopStreaming;
final void Function(String, PredefinedFormat?, Map)
onSubmitted;
final ValueNotifier> selectedSourcesNotifier;
final void Function(List) onUpdateSelectedSources;
final bool hideDecoration;
+ final bool hideFormats;
+ final Widget? extraBottomActionButton;
@override
State createState() => _DesktopPromptInputState();
@@ -42,7 +48,6 @@ class _DesktopPromptInputState extends State {
final overlayController = OverlayPortalController();
final inputControlCubit = ChatInputControlCubit();
final focusNode = FocusNode();
- final textController = TextEditingController();
late SendButtonState sendButtonState;
bool isComposing = false;
@@ -51,17 +56,19 @@ class _DesktopPromptInputState extends State {
void initState() {
super.initState();
- textController.addListener(handleTextControllerChanged);
- focusNode.addListener(
- () {
- if (!widget.hideDecoration) {
- setState(() {}); // refresh border color
- }
- if (!focusNode.hasFocus) {
- cancelMentionPage(); // hide menu when lost focus
- }
- },
- );
+ widget.textController.addListener(handleTextControllerChanged);
+ focusNode
+ ..addListener(
+ () {
+ if (!widget.hideDecoration) {
+ setState(() {}); // refresh border color
+ }
+ if (!focusNode.hasFocus) {
+ cancelMentionPage(); // hide menu when lost focus
+ }
+ },
+ )
+ ..onKeyEvent = handleKeyEvent;
updateSendButtonState();
@@ -79,7 +86,7 @@ class _DesktopPromptInputState extends State {
@override
void dispose() {
focusNode.dispose();
- textController.dispose();
+ widget.textController.removeListener(handleTextControllerChanged);
inputControlCubit.close();
super.dispose();
}
@@ -104,7 +111,7 @@ class _DesktopPromptInputState extends State {
overlayChildBuilder: (context) {
return PromptInputMentionPageMenu(
anchor: PromptInputAnchor(textFieldKey, layerLink),
- textController: textController,
+ textController: widget.textController,
onPageSelected: handlePageSelected,
);
},
@@ -134,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(
@@ -163,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
@@ -178,6 +186,8 @@ class _DesktopPromptInputState extends State {
widget.selectedSourcesNotifier,
onUpdateSelectedSources:
widget.onUpdateSelectedSources,
+ extraBottomActionButton:
+ widget.extraBottomActionButton,
),
),
),
@@ -216,12 +226,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();
}
});
@@ -237,7 +247,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;
@@ -249,9 +259,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;
}
@@ -274,7 +284,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) {
@@ -292,6 +302,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 =
@@ -338,22 +349,27 @@ class _DesktopPromptInputState extends State {
}
KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {
- if (event.character == '@') {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- inputControlCubit.startSearching(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;
}
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,
@@ -378,7 +394,7 @@ class _DesktopPromptInputState extends State {
key: textFieldKey,
editable: state.editable,
cubit: inputControlCubit,
- textController: textController,
+ textController: widget.textController,
textFieldFocusNode: focusNode,
contentPadding:
calculateContentPadding(state.showPredefinedFormats),
@@ -558,16 +574,19 @@ 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,
required this.onStopStreaming,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
+ this.extraBottomActionButton,
});
- final bool showPredefinedFormats;
+ final bool showPredefinedFormatBar;
+ final bool showPredefinedFormatButton;
final void Function() onTogglePredefinedFormatSection;
final void Function() onStartMention;
final SendButtonState sendButtonState;
@@ -575,6 +594,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) {
@@ -583,18 +603,27 @@ 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(),
+ if (showPredefinedFormatButton) ...[
+ _predefinedFormatButton(),
+ const HSpace(
+ DesktopAIChatSizes.inputActionBarButtonSpacing,
+ ),
+ ],
+ SelectModelMenu(
+ aiModelStateNotifier:
+ context.read().aiModelStateNotifier,
+ ),
const Spacer(),
if (state.aiType.isCloud) ...[
- _selectSourcesButton(context),
+ _selectSourcesButton(),
+ const HSpace(
+ DesktopAIChatSizes.inputActionBarButtonSpacing,
+ ),
+ ],
+ if (extraBottomActionButton != null) ...[
+ extraBottomActionButton!,
const HSpace(
DesktopAIChatSizes.inputActionBarButtonSpacing,
),
@@ -619,12 +648,12 @@ class _PromptBottomActions extends StatelessWidget {
Widget _predefinedFormatButton() {
return PromptInputDesktopToggleFormatButton(
- showFormatBar: showPredefinedFormats,
+ showFormatBar: showPredefinedFormatBar,
onTap: onTogglePredefinedFormatSection,
);
}
- Widget _selectSourcesButton(BuildContext context) {
+ Widget _selectSourcesButton() {
return PromptInputDesktopSelectSourcesButton(
onUpdateSelectedSources: onUpdateSelectedSources,
selectedSourcesNotifier: selectedSourcesNotifier,
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..a611d84310
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart
@@ -0,0 +1,264 @@
+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';
+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) {
+ 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(
+ model: state.selectedModel,
+ onTap: () {
+ if (state.selectedModel != null) {
+ popoverController.show();
+ }
+ },
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
+class SelectModelPopoverContent extends StatelessWidget {
+ const SelectModelPopoverContent({
+ super.key,
+ required this.models,
+ required this.selectedModel,
+ this.onSelectModel,
+ });
+
+ final List models;
+ final AIModelPB? selectedModel;
+ final void Function(AIModelPB)? 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 AIModelPB model;
+ final bool isSelected;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 32),
+ child: FlowyButton(
+ onTap: onTap,
+ margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
+ text: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ FlowyText(
+ model.i18n,
+ figmaLineHeight: 20,
+ overflow: TextOverflow.ellipsis,
+ ),
+ 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,
+ size: const Size.square(20),
+ color: Theme.of(context).colorScheme.primary,
+ )
+ : null,
+ ),
+ );
+ }
+}
+
+class _CurrentModelButton extends StatelessWidget {
+ const _CurrentModelButton({
+ required this.model,
+ required this.onTap,
+ });
+
+ final AIModelPB? model;
+ 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: 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(
+ // 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 != 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),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
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/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
index a27ab07e9d..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
@@ -133,7 +135,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/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/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/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 dd659420d6..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);
@@ -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,8 +199,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
),
);
showToastNotification(
- context,
- message: LocaleKeys.grid_url_copy.tr(),
+ 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/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart
index 991cf82b5d..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
@@ -182,7 +182,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
_divider(),
..._buildPublishActions(context),
- _divider(),
+
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
@@ -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.authenticator != AuthenticatorPB.AppFlowyCloud) {
+ if (userProfile == null || userProfile.authType != AuthTypePB.Server) {
return [];
}
@@ -236,6 +235,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.unpublish,
),
),
+ _divider(),
];
} else {
return [
@@ -246,6 +246,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.publish,
),
),
+ _divider(),
];
}
}
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/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 2d409f58b6..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();
@@ -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/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
index 97cc243c9e..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,6 +194,7 @@ class _MobileWorkspace extends StatelessWidget {
context.read().add(
UserWorkspaceEvent.openWorkspace(
workspace.workspaceId,
+ workspace.workspaceAuthType,
),
);
},
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..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()),
@@ -100,13 +94,12 @@ class _MobileHomeSettingPageState extends State {
key: ValueKey(currentWorkspaceId),
userProfile: userProfile,
workspaceId: currentWorkspaceId,
- currentWorkspaceMemberRole: state.currentWorkspace?.role,
),
const SupportSettingGroup(),
const AboutSettingGroup(),
UserSessionSettingGroup(
userProfile: userProfile,
- showThirdPartyLogin: isLocalAuthEnabled,
+ showThirdPartyLogin: false,
),
const VSpace(20),
],
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..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
@@ -16,6 +16,7 @@ enum _MobileSettingsPopupMenuItem {
members,
trash,
help,
+ helpAndDocumentation,
}
class HomePageSettingsPopupMenu extends StatelessWidget {
@@ -47,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 == AuthTypePB.Server) ...[
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.members,
@@ -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/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/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart
index 7ebfeefbbc..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,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 == 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 ef7f4492a5..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,6 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget {
context.read().add(
UserWorkspaceEvent.createWorkspace(
name,
+ AuthTypePB.Server,
),
);
},
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/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/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/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/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart
index 7f293cc1c9..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
@@ -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';
@@ -71,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(
@@ -94,6 +94,7 @@ class MobileSelectionMenu extends SelectionMenuService {
child: MobileSelectionMenuWidget(
selectionMenuStyle: style,
singleColumn: singleColumn,
+ showAtTop: showAtTop,
items: selectionMenuItems
..forEach((element) {
if (element is MobileSelectionMenuItem) {
@@ -166,7 +167,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 +207,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/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(),
+ ],
),
);
}
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/ai/ai_settings_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart
index f67cc9e6b8..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,
- showTopBorder: index == 0,
- isSelected: state.selectedAIModel == model,
+ 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));
+ .add(SettingsAIEvent.selectModel(entry.value));
context.pop();
},
),
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..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
@@ -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,
@@ -60,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/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/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart
index b3b7cb71c5..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.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
+ if (userProfile.authType == AuthTypePB.Server) ...[
const VSpace(16.0),
MobileLogoutButton(
text: LocaleKeys.button_deleteAccount.tr(),
@@ -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/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart
index 2e805c5c5a..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
@@ -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
@@ -282,11 +276,11 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
- return showToastNotification(
- context,
+ showToastNotification(
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
+ return;
}
context
.read()
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..2cfc349bf8
--- /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.objectId}) {
+ _parser = ChatNotificationParser(id: objectId, callback: _callback);
+ _subscription = RustStreamReceiver.listen(
+ (observable) => _parser?.parse(observable),
+ );
+ }
+
+ final String objectId;
+ 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/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart
index e7aca346e0..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
@@ -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;
@@ -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();
@@ -483,6 +483,7 @@ class ChatBloc extends Bloc {
void _regenerateAnswer(
String answerMessageIdString,
PredefinedFormat? format,
+ AIModelPB? 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;
+ }
await AIEventRegenerateResponse(payload).send().fold(
(success) {
@@ -637,6 +641,7 @@ class ChatEvent with _$ChatEvent {
const factory ChatEvent.regenerateAnswer(
String id,
PredefinedFormat? format,
+ AIModelPB? model,
) = _RegenerateAnswer;
// streaming answer
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/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart
index ce84f923d2..90085354db 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';
@@ -51,14 +48,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 != AuthTypePB.Server) {
+ // return Center(
+ // child: FlowyText(
+ // LocaleKeys.chat_unsupportedCloudPrompt.tr(),
+ // fontSize: 20,
+ // ),
+ // );
+ // }
return MultiBlocProvider(
providers: [
@@ -73,6 +70,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,
@@ -264,10 +262,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(),
),
@@ -384,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(
@@ -420,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_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_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/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/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..aa0d840574
--- /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/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(
+ 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> {
+ AIModelPB? 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 AIModelPB? selectedModel;
+ final void Function(AIModelPB) onSelectModel;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: models
+ .mapIndexed(
+ (index, model) => _buildModelButton(model, index == 0),
+ )
+ .toList(),
+ );
+ }
+
+ Widget _buildModelButton(
+ AIModelPB 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..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
@@ -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';
@@ -41,6 +42,7 @@ class AIMessageActionBar extends StatefulWidget {
required this.showDecoration,
this.onRegenerate,
this.onChangeFormat,
+ this.onChangeModel,
this.onOverrideVisibility,
});
@@ -48,6 +50,7 @@ class AIMessageActionBar extends StatefulWidget {
final bool showDecoration;
final void Function()? onRegenerate;
final void Function(PredefinedFormat)? onChangeFormat;
+ final void Function(AIModelPB)? onChangeModel;
final void Function(bool)? onOverrideVisibility;
@override
@@ -126,6 +129,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,
@@ -175,8 +184,7 @@ class CopyButton extends StatelessWidget {
);
if (context.mounted) {
showToastNotification(
- context,
- message: LocaleKeys.grid_url_copiedNotification.tr(),
+ message: LocaleKeys.message_copy_success.tr(),
);
}
},
@@ -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(AIModelPB)? 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..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
@@ -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';
@@ -23,6 +24,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 +43,7 @@ class ChatAIMessageBubble extends StatelessWidget {
this.isSelectingMessages = false,
this.onRegenerate,
this.onChangeFormat,
+ this.onChangeModel,
});
final Message message;
@@ -50,6 +53,7 @@ class ChatAIMessageBubble extends StatelessWidget {
final bool isSelectingMessages;
final void Function()? onRegenerate;
final void Function(PredefinedFormat)? onChangeFormat;
+ final void Function(AIModelPB)? onChangeModel;
@override
Widget build(BuildContext context) {
@@ -73,6 +77,7 @@ class ChatAIMessageBubble extends StatelessWidget {
message: message,
onRegenerate: onRegenerate,
onChangeFormat: onChangeFormat,
+ onChangeModel: onChangeModel,
child: child,
);
}
@@ -82,6 +87,7 @@ class ChatAIMessageBubble extends StatelessWidget {
message: message,
onRegenerate: onRegenerate,
onChangeFormat: onChangeFormat,
+ onChangeModel: onChangeModel,
child: child,
);
}
@@ -91,6 +97,7 @@ class ChatAIMessageBubble extends StatelessWidget {
message: message,
onRegenerate: onRegenerate,
onChangeFormat: onChangeFormat,
+ onChangeModel: onChangeModel,
child: child,
);
}
@@ -103,12 +110,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(AIModelPB)? onChangeModel;
@override
Widget build(BuildContext context) {
@@ -127,6 +136,7 @@ class ChatAIBottomInlineActions extends StatelessWidget {
showDecoration: false,
onRegenerate: onRegenerate,
onChangeFormat: onChangeFormat,
+ onChangeModel: onChangeModel,
),
),
const VSpace(32.0),
@@ -142,12 +152,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(AIModelPB)? onChangeModel;
@override
State createState() => _ChatAIMessageHoverState();
@@ -229,6 +241,7 @@ class _ChatAIMessageHoverState extends State {
showDecoration: true,
onRegenerate: widget.onRegenerate,
onChangeFormat: widget.onChangeFormat,
+ onChangeModel: widget.onChangeModel,
onOverrideVisibility: (visibility) {
overrideVisibility = visibility;
},
@@ -302,12 +315,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(AIModelPB)? onChangeModel;
@override
Widget build(BuildContext context) {
@@ -328,6 +343,8 @@ class ChatAIMessagePopup extends StatelessWidget {
_divider(),
_changeFormatButton(context),
_divider(),
+ _changeModelButton(context),
+ _divider(),
_saveToPageButton(context),
],
);
@@ -359,8 +376,7 @@ class ChatAIMessagePopup extends StatelessWidget {
}
if (context.mounted) {
showToastNotification(
- context,
- message: LocaleKeys.grid_url_copiedNotification.tr(),
+ message: LocaleKeys.message_copy_success.tr(),
);
}
},
@@ -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..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';
@@ -36,6 +37,7 @@ class ChatAIMessageWidget extends StatelessWidget {
this.onSelectedMetadata,
this.onRegenerate,
this.onChangeFormat,
+ this.onChangeModel,
this.isLastMessage = false,
this.isStreaming = false,
this.isSelectingMessages = false,
@@ -53,6 +55,7 @@ class ChatAIMessageWidget extends StatelessWidget {
final void Function()? onRegenerate;
final void Function() onStopStream;
final void Function(PredefinedFormat)? onChangeFormat;
+ final void Function(AIModelPB)? onChangeModel;
final bool isStreaming;
final bool isLastMessage;
final bool isSelectingMessages;
@@ -110,6 +113,7 @@ class ChatAIMessageWidget extends StatelessWidget {
isSelectingMessages: isSelectingMessages,
onRegenerate: onRegenerate,
onChangeFormat: onChangeFormat,
+ onChangeModel: onChangeModel,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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/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(),
- ),
- );
}
},
);
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/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,
+ ),
+ );
}
}
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/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/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/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..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
@@ -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 == AuthTypePB.Server &&
+ databaseId != null,
),
);
if (databaseId != null) {
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();
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) {
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/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/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/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/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/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart
index 8d64c537c3..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?.authenticator ?? AuthenticatorPB.Local) ==
- AuthenticatorPB.Local;
+ (widget.userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local;
@override
void dispose() {
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..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,8 +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';
@@ -18,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';
@@ -48,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(
@@ -104,7 +97,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),
+ ),
);
},
),
@@ -121,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),
+ ],
+ ),
),
);
}
@@ -208,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,
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..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?.authenticator ?? AuthenticatorPB.Local;
- return type == AuthenticatorPB.Local;
+ final type = userProfilePB?.authType ?? AuthTypePB.Local;
+ return type == AuthTypePB.Local;
}
@override
@@ -272,7 +272,9 @@ class DocumentBloc extends Bloc {
}
if (options.inMemoryUpdate) {
- Log.trace('skip transaction for in-memory update');
+ if (enableDocumentInternalLog) {
+ Log.trace('skip transaction for in-memory update');
+ }
return;
}
@@ -440,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/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart
index 74a6199b89..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?.authenticator == 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 0fae90920d..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?.authenticator == AuthenticatorPB.AppFlowyCloud,
+ shouldShowIndicator: userProfile?.authType == AuthTypePB.Server,
),
);
_syncStateListener.start(
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_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart
index 67ab383eba..5e7eefc24e 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) {
@@ -982,21 +983,6 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
return const EdgeInsets.symmetric(vertical: 10);
},
),
- 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_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
index 09685aef5c..edb19232be 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
@@ -20,6 +20,7 @@ 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:flutter/material.dart';
@@ -350,6 +351,7 @@ class _AppFlowyEditorPageState extends State
final isViewDeleted = context.read().state.isDeleted;
final isLocked =
context.read()?.state.isLocked ?? false;
+
final editor = Directionality(
textDirection: textDirection,
child: AppFlowyEditor(
@@ -393,7 +395,7 @@ class _AppFlowyEditorPageState extends State
},
child: SizedBox(
width: double.infinity,
- height: UniversalPlatform.isDesktopOrWeb ? 300 : 400,
+ height: UniversalPlatform.isDesktopOrWeb ? 600 : 400,
),
),
dropTargetStyle: AppFlowyDropTargetStyle(
@@ -428,37 +430,46 @@ class _AppFlowyEditorPageState extends State
),
);
}
-
+ final appTheme = AppFlowyTheme.of(context);
return Center(
- child: FloatingToolbar(
- floatingToolbarHeight: 40,
- padding: EdgeInsets.symmetric(horizontal: 6),
- style: FloatingToolbarStyle(
- backgroundColor: Theme.of(context).cardColor,
- toolbarActiveColor: Color(0xffe0f8fd),
- toolbarElevation: 10,
- ),
- items: toolbarItems,
- decoration: ShapeDecoration(
- color: Theme.of(context).cardColor,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
- ),
- toolbarBuilder: (context, child) => DesktopFloatingToolbar(
+ 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(appTheme.borderRadius.l),
+ color: appTheme.surfaceColorScheme.primary,
+ boxShadow: appTheme.shadow.small,
+ ),
+ toolbarBuilder: (_, child, onDismiss, isMetricsChanged) =>
+ BlocProvider.value(
+ value: context.read(),
+ child: DesktopFloatingToolbar(
+ editorState: editorState,
+ onDismiss: onDismiss,
+ enableAnimation: !isMetricsChanged,
+ child: child,
+ ),
+ ),
+ placeHolderBuilder: (_) => customPlaceholderItem,
editorState: editorState,
- 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/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()),
],
);
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_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart
index aee2e2cb50..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
@@ -2,14 +2,11 @@ import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
-import 'package:appflowy/plugins/document/application/prelude.dart';
+import 'package:appflowy/plugins/document/application/document_bloc.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';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,8 +15,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_gesture_detector.dart';
+import 'widgets/ai_writer_suggestion_actions.dart';
+import 'widgets/ai_writer_prompt_input_more_button.dart';
class AiWriterBlockKeys {
const AiWriterBlockKeys._();
@@ -98,18 +95,12 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget {
}
class _AIWriterBlockComponentState extends State {
- final key = GlobalKey();
final textController = TextEditingController();
final overlayController = OverlayPortalController();
final layerLink = LayerLink();
+ final focusNode = FocusNode();
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() {
@@ -117,16 +108,14 @@ class _AIWriterBlockComponentState extends State {
WidgetsBinding.instance.addPostFrameCallback((_) {
overlayController.show();
- if (!widget.node.isAiWriterInitialized) {
- aiWriterCubit.init();
- }
+ context.read().register(widget.node);
});
}
@override
void dispose() {
textController.dispose();
- aiWriterCubit.close();
+ focusNode.dispose();
super.dispose();
}
@@ -136,85 +125,49 @@ class _AIWriterBlockComponentState extends State {
return const SizedBox.shrink();
}
- return MultiBlocProvider(
- providers: [
- BlocProvider.value(
- value: aiWriterCubit,
- ),
- BlocProvider(
- create: (_) => AIPromptInputBloc(
- predefinedFormat: null,
- ),
- ),
- ],
+ final documentId = context.read()?.documentId;
+
+ return BlocProvider(
+ create: (_) => AIPromptInputBloc(
+ predefinedFormat: null,
+ objectId: documentId ?? editorState.document.root.id,
+ ),
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: Focus(
+ focusNode: focusNode,
+ child: OverlayContent(
+ editorState: editorState,
+ node: widget.node,
+ textController: textController,
),
),
- ],
- );
- },
- 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,80 +175,84 @@ 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 {
+class OverlayContent extends StatefulWidget {
const OverlayContent({
super.key,
required this.editorState,
required this.node,
+ required this.textController,
});
final EditorState editorState;
final Node node;
+ final TextEditingController textController;
+
+ @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(
builder: (context, state) {
- final selection = node.aiWriterSelection;
- final showSuggestionPopup =
- state is ReadyAiWriterState && !state.isFirstRun;
- final showActionPopup = state is ReadyAiWriterState && state.isFirstRun;
+ if (state is IdleAiWriterState ||
+ state is DocumentContentEmptyAiWriterState) {
+ return const SizedBox.shrink();
+ }
+
+ final command = (state as RegisteredAiWriter).command;
+
+ final selection = widget.node.aiWriterSelection;
+ 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 lightBorderColor =
- Theme.of(context).brightness == Brightness.light
- ? ColorSchemeConstants.lightBorderColor
- : ColorSchemeConstants.darkBorderColor;
+ final showSuggestedActions =
+ state is ReadyAiWriterState && !state.isFirstRun;
+ final isInitialReadyState =
+ state is ReadyAiWriterState && state.isFirstRun;
+ final showSuggestedActionsPopup =
+ showSuggestedActions && markdownText.isEmpty ||
+ (markdownText.isNotEmpty && command != AiWriterCommand.explain);
+ final showSuggestedActionsWithin = showSuggestedActions &&
+ markdownText.isNotEmpty &&
+ command == AiWriterCommand.explain;
+
+ final borderColor = 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(
context,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
- borderColor: darkBorderColor,
+ borderColor: borderColor,
),
child: SuggestionActionBar(
- actions: _getSuggestedActions(
- currentCommand: state.command,
- hasSelection: hasSelection,
- ),
+ currentCommand: command,
+ hasSelection: hasSelection,
onTap: (action) {
_onSelectSuggestionAction(context, action);
},
@@ -303,93 +260,74 @@ class OverlayContent extends StatelessWidget {
),
const VSpace(4.0 + 1.0),
],
- DecoratedBox(
+ Container(
decoration: _getModalDecoration(
context,
color: null,
- borderColor: darkBorderColor,
+ borderColor: borderColor,
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.command.i18n,
- fontSize: 12,
- fontWeight: FontWeight.w600,
- color: Color(0xFF666D76),
- ),
- ),
- const VSpace(4.0),
- Padding(
- padding:
- EdgeInsets.symmetric(horizontal: 6.0),
- child: AIMarkdownText(
- markdown: markdownText,
- ),
- ),
- ],
- ),
- ),
- ),
- if (showSuggestionPopup) ...[
- const VSpace(4.0),
- SuggestionActionBar(
- actions: _getSuggestedActions(
- currentCommand: state.command,
- hasSelection: hasSelection,
- ),
- onTap: (action) {
- _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(),
+ child: MainContentArea(
+ textController: widget.textController,
+ 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: MoreAiWriterCommands(
+ hasSelection: hasSelection,
+ editorState: widget.editorState,
+ onSelectCommand: (command) {
+ final state = context.read().state;
+ final showPredefinedFormats = state.showPredefinedFormats;
+ final predefinedFormat = state.predefinedFormat;
+ final text = widget.textController.text;
+
+ context.read().runCommand(
+ command,
+ text,
+ showPredefinedFormats ? predefinedFormat : null,
+ );
+ },
+ ),
+ );
+ },
),
],
);
@@ -397,39 +335,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,
- showPredefinedFormats ? predefinedFormat : null,
- );
- },
- ),
- );
- },
- );
- }
-
BoxDecoration _getModalDecoration(
BuildContext context, {
required Color? color,
@@ -443,13 +348,9 @@ class OverlayContent extends StatelessWidget {
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,
);
}
@@ -460,133 +361,20 @@ class OverlayContent extends StatelessWidget {
);
}
- 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)),
);
}
- 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,
@@ -598,10 +386,99 @@ 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